1 /*
2  * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.internal.util.xml;
27 
28 import java.io.*;
29 import java.nio.charset.Charset;
30 import java.util.InvalidPropertiesFormatException;
31 import java.util.Map.Entry;
32 import java.util.Properties;
33 import jdk.internal.org.xml.sax.Attributes;
34 import jdk.internal.org.xml.sax.InputSource;
35 import jdk.internal.org.xml.sax.SAXException;
36 import jdk.internal.org.xml.sax.SAXParseException;
37 import jdk.internal.org.xml.sax.helpers.DefaultHandler;
38 import jdk.internal.util.xml.impl.SAXParserImpl;
39 import jdk.internal.util.xml.impl.XMLStreamWriterImpl;
40 
41 /**
42  * A class used to aid in Properties load and save in XML. This class is
43  * re-implemented using a subset of SAX
44  *
45  * @author Joe Wang
46  * @since 1.8
47  */
48 public class PropertiesDefaultHandler extends DefaultHandler {
49 
50     // Elements specified in the properties.dtd
51     private static final String ELEMENT_ROOT = "properties";
52     private static final String ELEMENT_COMMENT = "comment";
53     private static final String ELEMENT_ENTRY = "entry";
54     private static final String ATTR_KEY = "key";
55     // The required DTD URI for exported properties
56     private static final String PROPS_DTD_DECL =
57         "<!DOCTYPE properties SYSTEM \"http://java.sun.com/dtd/properties.dtd\">";
58     private static final String PROPS_DTD_URI =
59         "http://java.sun.com/dtd/properties.dtd";
60     private static final String PROPS_DTD =
61         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
62             + "<!-- DTD for properties -->"
63             + "<!ELEMENT properties ( comment?, entry* ) >"
64             + "<!ATTLIST properties"
65             + " version CDATA #FIXED \"1.0\">"
66             + "<!ELEMENT comment (#PCDATA) >"
67             + "<!ELEMENT entry (#PCDATA) >"
68             + "<!ATTLIST entry "
69             + " key CDATA #REQUIRED>";
70     /**
71      * Version number for the format of exported properties files.
72      */
73     private static final String EXTERNAL_XML_VERSION = "1.0";
74     private Properties properties;
75 
load(Properties props, InputStream in)76     public void load(Properties props, InputStream in)
77         throws IOException, InvalidPropertiesFormatException, UnsupportedEncodingException
78     {
79         this.properties = props;
80 
81         try {
82             SAXParser parser = new SAXParserImpl();
83             parser.parse(in, this);
84         } catch (SAXException saxe) {
85             throw new InvalidPropertiesFormatException(saxe);
86         }
87 
88         /**
89          * String xmlVersion = propertiesElement.getAttribute("version"); if
90          * (xmlVersion.compareTo(EXTERNAL_XML_VERSION) > 0) throw new
91          * InvalidPropertiesFormatException( "Exported Properties file format
92          * version " + xmlVersion + " is not supported. This java installation
93          * can read" + " versions " + EXTERNAL_XML_VERSION + " or older. You" +
94          * " may need to install a newer version of JDK.");
95          */
96     }
97 
store(Properties props, OutputStream os, String comment, Charset charset)98     public void store(Properties props, OutputStream os, String comment, Charset charset)
99         throws IOException
100     {
101         try {
102             XMLStreamWriter writer = new XMLStreamWriterImpl(os, charset);
103             writer.writeStartDocument();
104             writer.writeDTD(PROPS_DTD_DECL);
105             writer.writeStartElement(ELEMENT_ROOT);
106             if (comment != null && !comment.isEmpty()) {
107                 writer.writeStartElement(ELEMENT_COMMENT);
108                 writer.writeCharacters(comment);
109                 writer.writeEndElement();
110             }
111 
112             synchronized(props) {
113                 for (Entry<Object, Object> e : props.entrySet()) {
114                     final Object k = e.getKey();
115                     final Object v = e.getValue();
116                     if (k instanceof String && v instanceof String) {
117                         writer.writeStartElement(ELEMENT_ENTRY);
118                         writer.writeAttribute(ATTR_KEY, (String)k);
119                         writer.writeCharacters((String)v);
120                         writer.writeEndElement();
121                     }
122                 }
123             }
124 
125             writer.writeEndElement();
126             writer.writeEndDocument();
127             writer.flush();
128         } catch (XMLStreamException e) {
129             if (e.getCause() instanceof UnsupportedEncodingException) {
130                 throw (UnsupportedEncodingException) e.getCause();
131             }
132             throw new IOException(e);
133         }
134 
135     }
136     ////////////////////////////////////////////////////////////////////
137     // Validate while parsing
138     ////////////////////////////////////////////////////////////////////
139     static final String ALLOWED_ELEMENTS = "comment, entry";
140     static final String ALLOWED_COMMENT = "comment";
141     ////////////////////////////////////////////////////////////////////
142     // Handler methods
143     ////////////////////////////////////////////////////////////////////
144     StringBuilder buf = new StringBuilder();
145     boolean sawRoot = false; // whether a valid root element exists
146     boolean sawComment = false;
147     boolean validEntry = false;
148     String key;
149     String rootElm;
150 
151     @Override
startElement(String uri, String localName, String qName, Attributes attributes)152     public void startElement(String uri, String localName, String qName, Attributes attributes)
153         throws SAXException
154     {
155         if (sawRoot) {
156             if (!ALLOWED_ELEMENTS.contains(qName)) {
157                 fatalError(new SAXParseException("Element type \"" + qName + "\" must be declared.", null));
158             }
159         } else {
160             // check whether the root has been declared in the DTD
161             if (rootElm == null) {
162                 fatalError(new SAXParseException("An XML properties document must contain"
163                     + " the DOCTYPE declaration as defined by java.util.Properties.", null));
164             }
165 
166             // check whether the element name matches the declaration
167             if (!rootElm.equals(qName)) {
168                 fatalError(new SAXParseException("Document root element \"" + qName
169                     + "\", must match DOCTYPE root \"" + rootElm + "\"", null));
170             }
171 
172             // this is a valid root element
173             sawRoot = true;
174         }
175 
176         if (qName.equals(ELEMENT_ENTRY)) {
177             validEntry = true;
178             key = attributes.getValue(ATTR_KEY);
179             if (key == null) {
180                 fatalError(new SAXParseException("Attribute \"key\" is required and " +
181                     "must be specified for element type \"entry\"", null));
182             }
183         } else if (qName.equals(ALLOWED_COMMENT)) {
184             if (sawComment) {
185                 fatalError(new SAXParseException("Only one comment element may be allowed. "
186                     + "The content of element type \"properties\" must match \"(comment?,entry*)\"", null));
187             }
188             sawComment = true;
189         }
190     }
191 
192     @Override
characters(char[] ch, int start, int length)193     public void characters(char[] ch, int start, int length) throws SAXException {
194         if (validEntry) {
195             buf.append(ch, start, length);
196         }
197     }
198 
199     @Override
endElement(String uri, String localName, String qName)200     public void endElement(String uri, String localName, String qName) throws SAXException {
201         if (!ALLOWED_ELEMENTS.contains(qName) && !ELEMENT_ROOT.equals(qName)) {
202             fatalError(new SAXParseException("Element: " + qName +
203                 " is invalid, must match  \"(comment?,entry*)\".", null));
204         }
205 
206         if (validEntry) {
207             properties.setProperty(key, buf.toString());
208             buf.delete(0, buf.length());
209             validEntry = false;
210         }
211     }
212 
213     @Override
resolveEntity(String pubid, String sysid)214     public InputSource resolveEntity(String pubid, String sysid)
215         throws SAXException, IOException {
216         {
217             if (sysid.equals(PROPS_DTD_URI)) {
218                 // The properties DTD is known to the handler, no need to parse it
219                 return null;
220             }
221             throw new SAXException("Invalid system identifier: " + sysid);
222         }
223     }
224 
225     @Override
error(SAXParseException x)226     public void error(SAXParseException x) throws SAXException {
227         throw x;
228     }
229 
230     @Override
fatalError(SAXParseException x)231     public void fatalError(SAXParseException x) throws SAXException {
232         throw x;
233     }
234 
235     @Override
warning(SAXParseException x)236     public void warning(SAXParseException x) throws SAXException {
237         throw x;
238     }
239 
240     // SAX2 extension from DTDHandler
241 
242     @Override
startDTD(String name, String publicId, String systemId)243     public void startDTD (String name, String publicId, String systemId) throws SAXException
244     {
245         if (!ELEMENT_ROOT.equals(name) || !PROPS_DTD_URI.equals(systemId)) {
246             fatalError(new SAXParseException("An XML properties document must contain"
247                 + " the DOCTYPE declaration as defined by java.util.Properties.", null));
248         }
249         rootElm = name;
250     }
251 
252     @Override
startInternalSub()253     public void startInternalSub () throws SAXException
254     {
255         fatalError(new SAXParseException("Internal DTD subset is not allowed. " +
256             "The Properties XML document must have the following DOCTYPE declaration: \n" +
257             PROPS_DTD_DECL, null));
258     }
259 }
260