001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements. See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership. The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the  "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    /*
019     * $Id: OutputProperties.java 468643 2006-10-28 06:56:03Z minchau $
020     */
021    package org.apache.xalan.templates;
022    
023    import java.util.Enumeration;
024    import java.util.Properties;
025    import java.util.Vector;
026    
027    import javax.xml.transform.OutputKeys;
028    import javax.xml.transform.TransformerException;
029    
030    import org.apache.xalan.res.XSLMessages;
031    import org.apache.xalan.res.XSLTErrorResources;
032    import org.apache.xml.serializer.OutputPropertiesFactory;
033    import org.apache.xml.serializer.OutputPropertyUtils;
034    import org.apache.xml.utils.FastStringBuffer;
035    import org.apache.xml.utils.QName;
036    
037    /**
038     * This class provides information from xsl:output elements. It is mainly
039     * a wrapper for {@link java.util.Properties}, but can not extend that class
040     * because it must be part of the {@link org.apache.xalan.templates.ElemTemplateElement}
041     * heararchy.
042     * <p>An OutputProperties list can contain another OutputProperties list as
043     * its "defaults"; this second property list is searched if the property key
044     * is not found in the original property list.</p>
045     * @see <a href="http://www.w3.org/TR/xslt#dtd">XSLT DTD</a>
046     * @see <a href="http://www.w3.org/TR/xslt#output">xsl:output in XSLT Specification</a>
047     *
048     */
049    public class OutputProperties extends ElemTemplateElement
050            implements Cloneable
051    {
052        static final long serialVersionUID = -6975274363881785488L;
053      /**
054       * Creates an empty OutputProperties with no default values.
055       */
056      public OutputProperties()
057      {
058        this(org.apache.xml.serializer.Method.XML);
059      }
060    
061      /**
062       * Creates an empty OutputProperties with the specified defaults.
063       *
064       * @param   defaults   the defaults.
065       */
066      public OutputProperties(Properties defaults)
067      {
068        m_properties = new Properties(defaults);
069      }
070    
071      /**
072       * Creates an empty OutputProperties with the defaults specified by
073       * a property file.  The method argument is used to construct a string of
074       * the form output_[method].properties (for instance, output_html.properties).
075       * The output_xml.properties file is always used as the base.
076       * <p>At the moment, anything other than 'text', 'xml', and 'html', will
077       * use the output_xml.properties file.</p>
078       *
079       * @param   method non-null reference to method name.
080       */
081      public OutputProperties(String method)
082      {
083        m_properties = new Properties(
084            OutputPropertiesFactory.getDefaultMethodProperties(method));
085      }
086    
087      /**
088       * Clone this OutputProperties, including a clone of the wrapped Properties
089       * reference.
090       *
091       * @return A new OutputProperties reference, mutation of which should not
092       *         effect this object.
093       */
094      public Object clone()
095      {
096    
097        try
098        {
099          OutputProperties cloned = (OutputProperties) super.clone();
100    
101          cloned.m_properties = (Properties) cloned.m_properties.clone();
102    
103          return cloned;
104        }
105        catch (CloneNotSupportedException e)
106        {
107          return null;
108        }
109      }
110    
111      /**
112       * Set an output property.
113       *
114       * @param key the key to be placed into the property list.
115       * @param value the value corresponding to <tt>key</tt>.
116       * @see javax.xml.transform.OutputKeys
117       */
118      public void setProperty(QName key, String value)
119      {
120        setProperty(key.toNamespacedString(), value);
121      }
122    
123      /**
124       * Set an output property.
125       *
126       * @param key the key to be placed into the property list.
127       * @param value the value corresponding to <tt>key</tt>.
128       * @see javax.xml.transform.OutputKeys
129       */
130      public void setProperty(String key, String value)
131      {
132        if(key.equals(OutputKeys.METHOD))
133        {
134          setMethodDefaults(value);
135        }
136        
137        if (key.startsWith(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL))
138          key = OutputPropertiesFactory.S_BUILTIN_EXTENSIONS_UNIVERSAL
139             + key.substring(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL_LEN);
140        
141        m_properties.put(key, value);
142      }
143    
144      /**
145       * Searches for the property with the specified key in the property list.
146       * If the key is not found in this property list, the default property list,
147       * and its defaults, recursively, are then checked. The method returns
148       * <code>null</code> if the property is not found.
149       *
150       * @param   key   the property key.
151       * @return  the value in this property list with the specified key value.
152       */
153      public String getProperty(QName key)
154      {
155        return m_properties.getProperty(key.toNamespacedString());
156      }
157    
158      /**
159       * Searches for the property with the specified key in the property list.
160       * If the key is not found in this property list, the default property list,
161       * and its defaults, recursively, are then checked. The method returns
162       * <code>null</code> if the property is not found.
163       *
164       * @param   key   the property key.
165       * @return  the value in this property list with the specified key value.
166       */
167      public String getProperty(String key) 
168      {
169        if (key.startsWith(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL))
170          key = OutputPropertiesFactory.S_BUILTIN_EXTENSIONS_UNIVERSAL 
171            + key.substring(OutputPropertiesFactory.S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL_LEN);
172        return m_properties.getProperty(key);
173      }
174    
175      /**
176       * Set an output property.
177       *
178       * @param key the key to be placed into the property list.
179       * @param value the value corresponding to <tt>key</tt>.
180       * @see javax.xml.transform.OutputKeys
181       */
182      public void setBooleanProperty(QName key, boolean value)
183      {
184        m_properties.put(key.toNamespacedString(), value ? "yes" : "no");
185      }
186    
187      /**
188       * Set an output property.
189       *
190       * @param key the key to be placed into the property list.
191       * @param value the value corresponding to <tt>key</tt>.
192       * @see javax.xml.transform.OutputKeys
193       */
194      public void setBooleanProperty(String key, boolean value)
195      {
196        m_properties.put(key, value ? "yes" : "no");
197      }
198    
199      /**
200       * Searches for the boolean property with the specified key in the property list.
201       * If the key is not found in this property list, the default property list,
202       * and its defaults, recursively, are then checked. The method returns
203       * <code>false</code> if the property is not found, or if the value is other
204       * than "yes".
205       *
206       * @param   key   the property key.
207       * @return  the value in this property list as a boolean value, or false
208       * if null or not "yes".
209       */
210      public boolean getBooleanProperty(QName key)
211      {
212        return getBooleanProperty(key.toNamespacedString());
213      }
214    
215      /**
216       * Searches for the boolean property with the specified key in the property list.
217       * If the key is not found in this property list, the default property list,
218       * and its defaults, recursively, are then checked. The method returns
219       * <code>false</code> if the property is not found, or if the value is other
220       * than "yes".
221       *
222       * @param   key   the property key.
223       * @return  the value in this property list as a boolean value, or false
224       * if null or not "yes".
225       */
226      public boolean getBooleanProperty(String key)
227      {
228        return OutputPropertyUtils.getBooleanProperty(key, m_properties);
229      }
230    
231      /**
232       * Set an output property.
233       *
234       * @param key the key to be placed into the property list.
235       * @param value the value corresponding to <tt>key</tt>.
236       * @see javax.xml.transform.OutputKeys
237       */
238      public void setIntProperty(QName key, int value)
239      {
240        setIntProperty(key.toNamespacedString(), value);
241      }
242    
243      /**
244       * Set an output property.
245       *
246       * @param key the key to be placed into the property list.
247       * @param value the value corresponding to <tt>key</tt>.
248       * @see javax.xml.transform.OutputKeys
249       */
250      public void setIntProperty(String key, int value)
251      {
252        m_properties.put(key, Integer.toString(value));
253      }
254    
255      /**
256       * Searches for the int property with the specified key in the property list.
257       * If the key is not found in this property list, the default property list,
258       * and its defaults, recursively, are then checked. The method returns
259       * <code>false</code> if the property is not found, or if the value is other
260       * than "yes".
261       *
262       * @param   key   the property key.
263       * @return  the value in this property list as a int value, or false
264       * if null or not a number.
265       */
266      public int getIntProperty(QName key)
267      {
268        return getIntProperty(key.toNamespacedString());
269      }
270    
271      /**
272       * Searches for the int property with the specified key in the property list.
273       * If the key is not found in this property list, the default property list,
274       * and its defaults, recursively, are then checked. The method returns
275       * <code>false</code> if the property is not found, or if the value is other
276       * than "yes".
277       *
278       * @param   key   the property key.
279       * @return  the value in this property list as a int value, or false
280       * if null or not a number.
281       */
282      public int getIntProperty(String key)
283      {
284        return OutputPropertyUtils.getIntProperty(key, m_properties);
285      }
286    
287    
288      /**
289       * Set an output property with a QName value.  The QName will be turned
290       * into a string with the namespace in curly brackets.
291       *
292       * @param key the key to be placed into the property list.
293       * @param value the value corresponding to <tt>key</tt>.
294       * @see javax.xml.transform.OutputKeys
295       */
296      public void setQNameProperty(QName key, QName value)
297      {
298        setQNameProperty(key.toNamespacedString(), value);
299      }
300      
301      /**
302       * Reset the default properties based on the method.
303       *
304       * @param method the method value.
305       * @see javax.xml.transform.OutputKeys
306       */
307      public void setMethodDefaults(String method)
308      {
309            String defaultMethod = m_properties.getProperty(OutputKeys.METHOD);
310     
311            if((null == defaultMethod) || !defaultMethod.equals(method)
312             // bjm - add the next condition as a hack
313             // but it is because both output_xml.properties and
314             // output_unknown.properties have the same method=xml
315             // for their default. Otherwise we end up with
316             // a ToUnknownStream wraping a ToXMLStream even
317             // when the users says method="xml"
318             //
319             || defaultMethod.equals("xml")
320             )
321            {
322                Properties savedProps = m_properties;
323                Properties newDefaults = 
324                    OutputPropertiesFactory.getDefaultMethodProperties(method);
325                m_properties = new Properties(newDefaults);
326                copyFrom(savedProps, false);
327            }
328      }
329      
330    
331      /**
332       * Set an output property with a QName value.  The QName will be turned
333       * into a string with the namespace in curly brackets.
334       *
335       * @param key the key to be placed into the property list.
336       * @param value the value corresponding to <tt>key</tt>.
337       * @see javax.xml.transform.OutputKeys
338       */
339      public void setQNameProperty(String key, QName value)
340      {
341        setProperty(key, value.toNamespacedString());
342      }
343    
344      /**
345       * Searches for the qname property with the specified key in the property list.
346       * If the key is not found in this property list, the default property list,
347       * and its defaults, recursively, are then checked. The method returns
348       * <code>null</code> if the property is not found.
349       *
350       * @param   key   the property key.
351       * @return  the value in this property list as a QName value, or false
352       * if null or not "yes".
353       */
354      public QName getQNameProperty(QName key)
355      {
356        return getQNameProperty(key.toNamespacedString());
357      }
358    
359      /**
360       * Searches for the qname property with the specified key in the property list.
361       * If the key is not found in this property list, the default property list,
362       * and its defaults, recursively, are then checked. The method returns
363       * <code>null</code> if the property is not found.
364       *
365       * @param   key   the property key.
366       * @return  the value in this property list as a QName value, or false
367       * if null or not "yes".
368       */
369      public QName getQNameProperty(String key)
370      {
371        return getQNameProperty(key, m_properties);
372      }
373    
374      /**
375       * Searches for the qname property with the specified key in the property list.
376       * If the key is not found in this property list, the default property list,
377       * and its defaults, recursively, are then checked. The method returns
378       * <code>null</code> if the property is not found.
379       *
380       * @param   key   the property key.
381       * @param props the list of properties to search in.
382       * @return  the value in this property list as a QName value, or false
383       * if null or not "yes".
384       */
385      public static QName getQNameProperty(String key, Properties props)
386      {
387    
388        String s = props.getProperty(key);
389    
390        if (null != s)
391          return QName.getQNameFromString(s);
392        else
393          return null;
394      }
395    
396      /**
397       * Set an output property with a QName list value.  The QNames will be turned
398       * into strings with the namespace in curly brackets.
399       *
400       * @param key the key to be placed into the property list.
401       * @param v non-null list of QNames corresponding to <tt>key</tt>.
402       * @see javax.xml.transform.OutputKeys
403       */
404      public void setQNameProperties(QName key, Vector v)
405      {
406        setQNameProperties(key.toNamespacedString(), v);
407      }
408    
409      /**
410       * Set an output property with a QName list value.  The QNames will be turned
411       * into strings with the namespace in curly brackets.
412       *
413       * @param key the key to be placed into the property list.
414       * @param v non-null list of QNames corresponding to <tt>key</tt>.
415       * @see javax.xml.transform.OutputKeys
416       */
417      public void setQNameProperties(String key, Vector v)
418      {
419    
420        int s = v.size();
421    
422        // Just an initial guess at reasonable tuning parameters
423        FastStringBuffer fsb = new FastStringBuffer(9,9);
424    
425        for (int i = 0; i < s; i++)
426        {
427          QName qname = (QName) v.elementAt(i);
428    
429          fsb.append(qname.toNamespacedString());
430          // Don't append space after last value
431          if (i < s-1) 
432            fsb.append(' ');
433        }
434    
435        m_properties.put(key, fsb.toString());
436      }
437    
438      /**
439       * Searches for the list of qname properties with the specified key in
440       * the property list.
441       * If the key is not found in this property list, the default property list,
442       * and its defaults, recursively, are then checked. The method returns
443       * <code>null</code> if the property is not found.
444       *
445       * @param   key   the property key.
446       * @return  the value in this property list as a vector of QNames, or false
447       * if null or not "yes".
448       */
449      public Vector getQNameProperties(QName key)
450      {
451        return getQNameProperties(key.toNamespacedString());
452      }
453    
454      /**
455       * Searches for the list of qname properties with the specified key in
456       * the property list.
457       * If the key is not found in this property list, the default property list,
458       * and its defaults, recursively, are then checked. The method returns
459       * <code>null</code> if the property is not found.
460       *
461       * @param   key   the property key.
462       * @return  the value in this property list as a vector of QNames, or false
463       * if null or not "yes".
464       */
465      public Vector getQNameProperties(String key)
466      {
467        return getQNameProperties(key, m_properties);
468      }
469    
470      /**
471       * Searches for the list of qname properties with the specified key in
472       * the property list.
473       * If the key is not found in this property list, the default property list,
474       * and its defaults, recursively, are then checked. The method returns
475       * <code>null</code> if the property is not found.
476       *
477       * @param   key   the property key.
478       * @param props the list of properties to search in.
479       * @return  the value in this property list as a vector of QNames, or false
480       * if null or not "yes".
481       */
482      public static Vector getQNameProperties(String key, Properties props)
483      {
484    
485        String s = props.getProperty(key);
486    
487        if (null != s)
488        {
489          Vector v = new Vector();
490          int l = s.length();
491          boolean inCurly = false;
492          FastStringBuffer buf = new FastStringBuffer();
493    
494          // parse through string, breaking on whitespaces.  I do this instead 
495          // of a tokenizer so I can track whitespace inside of curly brackets, 
496          // which theoretically shouldn't happen if they contain legal URLs.
497          for (int i = 0; i < l; i++)
498          {
499            char c = s.charAt(i);
500    
501            if (Character.isWhitespace(c))
502            {
503              if (!inCurly)
504              {
505                if (buf.length() > 0)
506                {
507                  QName qname = QName.getQNameFromString(buf.toString());
508                  v.addElement(qname);
509                  buf.reset();
510                }
511                continue;
512              }
513            }
514            else if ('{' == c)
515              inCurly = true;
516            else if ('}' == c)
517              inCurly = false;
518    
519            buf.append(c);
520          }
521    
522          if (buf.length() > 0)
523          {
524            QName qname = QName.getQNameFromString(buf.toString());
525            v.addElement(qname);
526            buf.reset();
527          }
528    
529          return v;
530        }
531        else
532          return null;
533      }
534    
535      /**
536       * This function is called to recompose all of the output format extended elements.
537       *
538       * @param root non-null reference to the stylesheet root object.
539       */
540      public void recompose(StylesheetRoot root)
541        throws TransformerException
542      {
543        root.recomposeOutput(this);
544      }
545    
546      /**
547       * This function is called after everything else has been
548       * recomposed, and allows the template to set remaining
549       * values that may be based on some other property that
550       * depends on recomposition.
551       */
552      public void compose(StylesheetRoot sroot) throws TransformerException
553      {
554    
555        super.compose(sroot);
556    
557      }
558    
559      /**
560       * Get the Properties object that this class wraps.
561       *
562       * @return non-null reference to Properties object.
563       */
564      public Properties getProperties()
565      {
566        return m_properties;
567      }
568      
569      /**
570       * Copy the keys and values from the source to this object.  This will
571       * not copy the default values.  This is meant to be used by going from
572       * a higher precedence object to a lower precedence object, so that if a
573       * key already exists, this method will not reset it.
574       *
575       * @param src non-null reference to the source properties.
576       */
577      public void copyFrom(Properties src)
578      {
579        copyFrom(src, true);
580      }
581    
582      /**
583       * Copy the keys and values from the source to this object.  This will
584       * not copy the default values.  This is meant to be used by going from
585       * a higher precedence object to a lower precedence object, so that if a
586       * key already exists, this method will not reset it.
587       *
588       * @param src non-null reference to the source properties.
589       * @param shouldResetDefaults true if the defaults should be reset based on 
590       *                            the method property.
591       */
592      public void copyFrom(Properties src, boolean shouldResetDefaults)
593      {
594    
595        Enumeration keys = src.keys();
596    
597        while (keys.hasMoreElements())
598        {
599          String key = (String) keys.nextElement();
600        
601          if (!isLegalPropertyKey(key))
602            throw new IllegalArgumentException(XSLMessages.createMessage(XSLTErrorResources.ER_OUTPUT_PROPERTY_NOT_RECOGNIZED, new Object[]{key})); //"output property not recognized: "
603          
604          Object oldValue = m_properties.get(key);
605          if (null == oldValue)
606          {
607            String val = (String) src.get(key);
608            
609            if(shouldResetDefaults && key.equals(OutputKeys.METHOD))
610            {
611              setMethodDefaults(val);
612            }
613    
614            m_properties.put(key, val);
615          }
616          else if (key.equals(OutputKeys.CDATA_SECTION_ELEMENTS))
617          {
618            m_properties.put(key, (String) oldValue + " " + (String) src.get(key));
619          }
620        }
621      }
622    
623      /**
624       * Copy the keys and values from the source to this object.  This will
625       * not copy the default values.  This is meant to be used by going from
626       * a higher precedence object to a lower precedence object, so that if a
627       * key already exists, this method will not reset it.
628       *
629       * @param opsrc non-null reference to an OutputProperties.
630       */
631      public void copyFrom(OutputProperties opsrc)
632        throws TransformerException
633      {
634       // Bugzilla 6157: recover from xsl:output statements
635        // checkDuplicates(opsrc);
636        copyFrom(opsrc.getProperties());
637      }
638    
639      /**
640       * Report if the key given as an argument is a legal xsl:output key.
641       *
642       * @param key non-null reference to key name.
643       *
644       * @return true if key is legal.
645       */
646      public static boolean isLegalPropertyKey(String key)
647      {
648    
649        return (key.equals(OutputKeys.CDATA_SECTION_ELEMENTS)
650                || key.equals(OutputKeys.DOCTYPE_PUBLIC)
651                || key.equals(OutputKeys.DOCTYPE_SYSTEM)
652                || key.equals(OutputKeys.ENCODING)
653                || key.equals(OutputKeys.INDENT)
654                || key.equals(OutputKeys.MEDIA_TYPE)
655                || key.equals(OutputKeys.METHOD)
656                || key.equals(OutputKeys.OMIT_XML_DECLARATION)
657                || key.equals(OutputKeys.STANDALONE)
658                || key.equals(OutputKeys.VERSION)
659                || (key.length() > 0) 
660                      && (key.charAt(0) == '{') 
661                      && (key.lastIndexOf('{') == 0)
662                      && (key.indexOf('}') > 0)
663                      && (key.lastIndexOf('}') == key.indexOf('}')));
664      }
665    
666      /** The output properties.
667       *  @serial */
668      private Properties m_properties = null;
669    
670        /**
671         * Creates an empty OutputProperties with the defaults specified by
672         * a property file.  The method argument is used to construct a string of
673         * the form output_[method].properties (for instance, output_html.properties).
674         * The output_xml.properties file is always used as the base.
675         * <p>At the moment, anything other than 'text', 'xml', and 'html', will
676         * use the output_xml.properties file.</p>
677         *
678         * @param   method non-null reference to method name.
679         *
680         * @return Properties object that holds the defaults for the given method.
681         * 
682         * @deprecated Use org.apache.xml.serializer.OuputPropertiesFactory.
683         * getDefaultMethodProperties directly.
684         */
685        static public Properties getDefaultMethodProperties(String method)
686        {
687            return org.apache.xml.serializer.OutputPropertiesFactory.getDefaultMethodProperties(method);
688        }
689    }