001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.engine.service.shared;
018
019import java.io.IOException;
020import java.io.Writer;
021import java.lang.reflect.Array;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.regex.Pattern;
029
030import com.thoughtworks.xstream.XStream;
031import com.thoughtworks.xstream.XStreamException;
032import com.thoughtworks.xstream.core.util.QuickWriter;
033import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
034import com.thoughtworks.xstream.io.naming.NameCoder;
035import com.thoughtworks.xstream.io.xml.DomDriver;
036import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
037
038/**
039 * Utility code for serializing to XML.
040 */
041public class XmlSerializationUtils {
042
043        /** We allow xstream to deserialize all classes from these packages. */
044        private static final String[] WHITELISTED_PACKAGES = new String[] { "com.teamscale.**", "org.conqat.**" };
045
046        /** We also allow to deserialize these specific classes. */
047        private static final String[] WHITELISTED_CLASSES = new String[] { "java.util.Collections$ReverseComparator" };
048
049        /**
050         * Patterns used for removing character escapes that are not allowed in XML 1.0.
051         * See <a href="http://stackoverflow.com/a/9987636/1237576">here</a> for the
052         * origin of the regex.
053         */
054        private static final Pattern INVALID_CHARACTER_ESCAPE_PATTERN = Pattern
055                        .compile("&#x(0?[0-8bcef]|1[0-9]|d[89abcd]..|fff[ef]);", Pattern.CASE_INSENSITIVE);
056
057        /**
058         * Serializes the given object to XML.
059         *
060         * @param cdataAttributes
061         *            all attributes with given name are converted to CDATA instead of
062         *            "normal" strings.
063         */
064        public static String serializeToXML(Object object, String... cdataAttributes) {
065                CDataAwareXStream xstream = new CDataAwareXStream();
066                return xstream.toXML(object, cdataAttributes);
067        }
068
069        /**
070         * Deserializes the given object from XML.
071         *
072         * @throws IOException
073         *             if deserialization returns a different class or the underlying
074         *             XStream library throws an {@link XStreamException} .
075         */
076        public static <T> T deserializeFromXML(String xml, Class<T> expectedClass) throws IOException {
077                return deserializeFromXMLWithAliases(xml, expectedClass, Collections.emptyMap());
078        }
079
080        /**
081         * Identical to {@link #deserializeFromXML(String, Class)} with the additional
082         * option of adding class aliases.
083         *
084         * @param elementAliases
085         *            Map of fully classified class name to actual class
086         *
087         * @throws IOException
088         *             if deserialization returns a different class or the underlying
089         *             XStream library throws an {@link XStreamException} .
090         */
091        public static <T> T deserializeFromXMLWithAliases(String xml, Class<T> expectedClass,
092                        Map<String, Class<?>> elementAliases) throws IOException {
093                XStream xstream = new CDataAwareXStream();
094                xstream.processAnnotations(expectedClass);
095                xstream.autodetectAnnotations(true);
096
097                for (Map.Entry<String, Class<?>> aliasEntry : elementAliases.entrySet()) {
098                        xstream.alias(aliasEntry.getKey(), aliasEntry.getValue());
099                }
100
101                // remove any entities that are not allowed in XML 1.0 and replace with
102                // "?". In most cases where we use this code, this will not cause any
103                // problems.
104                xml = INVALID_CHARACTER_ESCAPE_PATTERN.matcher(xml).replaceAll("?");
105
106                try {
107                        Object result = xstream.fromXML(xml);
108                        if (result == null) {
109                                // Leave as is
110                        } else if (result instanceof List && expectedClass.isArray()) {
111                                // Got a List but expected an array. Convert on the fly.
112                                List<?> list = (List<?>) result;
113                                result = list.toArray((Object[]) Array.newInstance(expectedClass.getComponentType(), list.size()));
114                        } else if (result instanceof Object[] && List.class.isAssignableFrom(expectedClass)) {
115                                // Got an array but expected a List. Convert on the fly.
116                                Object[] array = (Object[]) result;
117                                result = Arrays.asList(array);
118                        }
119                        return expectedClass.cast(result);
120                } catch (ClassCastException e) {
121                        throw new IOException("Invalid class returned during deserialization. Expected " + expectedClass, e);
122                } catch (XStreamException e) {
123                        throw new IOException(e);
124                }
125        }
126
127        /** An {@link XStream} that also support CDATA encoding. */
128        private static class CDataAwareXStream extends XStream {
129
130                /** Attributes that are to be encoded as CDATA. */
131                private final Set<String> cdataAttributes;
132
133                /** Constructor. */
134                public CDataAwareXStream() {
135                        this(new HashSet<>());
136                }
137
138                /**
139                 * Constructor. This separate constructor is a trick to pass the same set to the
140                 * object created for the super constructor and also hold a reference to it in
141                 * an attribute. If we would construct the set at the declaration point of the
142                 * attribute, this could not be passed to super, as super is called before
143                 * attributes are initialized.
144                 */
145                private CDataAwareXStream(Set<String> cdataAttributes) {
146                        super(new CDataAwareDomDriver(cdataAttributes));
147                        this.cdataAttributes = cdataAttributes;
148                        setMode(XStream.NO_REFERENCES);
149                        autodetectAnnotations(true);
150                        XStream.setupDefaultSecurity(this);
151                        allowTypesByWildcard(WHITELISTED_PACKAGES);
152                        allowTypes(WHITELISTED_CLASSES);
153                }
154
155                /**
156                 * Converts the given object to XML.
157                 *
158                 * @param cdataAttributes
159                 *            all attributes with given name are converted to CDATA instead of
160                 *            "normal" strings.
161                 */
162                public String toXML(Object object, String[] cdataAttributes) {
163                        try {
164                                this.cdataAttributes.addAll(Arrays.asList(cdataAttributes));
165                                return toXML(object);
166                        } finally {
167                                this.cdataAttributes.clear();
168                        }
169                }
170        }
171
172        /** A XStream driver that support CDATA output. */
173        private static class CDataAwareDomDriver extends DomDriver {
174
175                /** Attributes that are to be encoded as CDATA. */
176                private final Set<String> cdataAttributes;
177
178                /** Constructor. */
179                public CDataAwareDomDriver(Set<String> cdataAttributes) {
180                        this.cdataAttributes = cdataAttributes;
181                }
182
183                /** {@inheritDoc} */
184                @Override
185                public HierarchicalStreamWriter createWriter(Writer out) {
186                        return new CDataAwarePrettyPrintWriter(out, getNameCoder(), cdataAttributes);
187                }
188        }
189
190        /**
191         * A pretty printer that outputs attributes that contains "JSON" as CDATA.
192         */
193        private static class CDataAwarePrettyPrintWriter extends PrettyPrintWriter {
194
195                /** Flag for storing CData mode. */
196                private boolean nextIsCData = false;
197
198                /** Attributes that are to be encoded as CDATA. */
199                private final Set<String> cdataAttributes;
200
201                /** Constructor. */
202                public CDataAwarePrettyPrintWriter(Writer out, NameCoder nameCoder, Set<String> cdataAttributes) {
203                        super(out, nameCoder);
204                        this.cdataAttributes = cdataAttributes;
205                }
206
207                /** {@inheritDoc} */
208                @Override
209                public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
210                        super.startNode(name, clazz);
211                        nextIsCData = this.cdataAttributes.contains(name);
212                }
213
214                /** {@inheritDoc} */
215                @Override
216                protected void writeText(QuickWriter writer, String text) {
217                        if (nextIsCData) {
218                                writer.write("<![CDATA[");
219                                writer.write(text);
220                                writer.write("]]>");
221                        } else {
222                                super.writeText(writer, text);
223                        }
224                }
225        }
226}