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}