All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.engine.service.shared.XmlSerializationUtils Maven / Gradle / Ivy

/*
 * Copyright (c) CQSE GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.conqat.engine.service.shared;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.naming.NameCoder;
import com.thoughtworks.xstream.io.xml.DomDriver;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;

/**
 * Utility code for serializing to XML.
 */
public class XmlSerializationUtils {

	/** We allow xstream to deserialize all classes from these packages. */
	private static final String[] WHITELISTED_PACKAGES = new String[] { "com.teamscale.**", "org.conqat.**" };

	/** We also allow to deserialize these specific classes. */
	private static final String[] WHITELISTED_CLASSES = new String[] { "java.util.Collections$ReverseComparator" };

	/**
	 * Patterns used for removing character escapes that are not allowed in XML 1.0.
	 * See here for the
	 * origin of the regex.
	 */
	private static final Pattern INVALID_CHARACTER_ESCAPE_PATTERN = Pattern
			.compile("&#x(0?[0-8bcef]|1[0-9]|d[89abcd]..|fff[ef]);", Pattern.CASE_INSENSITIVE);

	/**
	 * Serializes the given object to XML.
	 *
	 * @param cdataAttributes
	 *            all attributes with given name are converted to CDATA instead of
	 *            "normal" strings.
	 */
	public static String serializeToXML(Object object, String... cdataAttributes) {
		CDataAwareXStream xstream = new CDataAwareXStream();
		return xstream.toXML(object, cdataAttributes);
	}

	/**
	 * Deserializes the given object from XML.
	 *
	 * @throws IOException
	 *             if deserialization returns a different class or the underlying
	 *             XStream library throws an {@link XStreamException} .
	 */
	public static  T deserializeFromXML(String xml, Class expectedClass) throws IOException {
		return deserializeFromXMLWithAliases(xml, expectedClass, Collections.emptyMap());
	}

	/**
	 * Identical to {@link #deserializeFromXML(String, Class)} with the additional
	 * option of adding class aliases.
	 *
	 * @param elementAliases
	 *            Map of fully classified class name to actual class
	 *
	 * @throws IOException
	 *             if deserialization returns a different class or the underlying
	 *             XStream library throws an {@link XStreamException} .
	 */
	public static  T deserializeFromXMLWithAliases(String xml, Class expectedClass,
			Map> elementAliases) throws IOException {
		XStream xstream = new CDataAwareXStream();
		xstream.processAnnotations(expectedClass);
		xstream.autodetectAnnotations(true);

		for (Map.Entry> aliasEntry : elementAliases.entrySet()) {
			xstream.alias(aliasEntry.getKey(), aliasEntry.getValue());
		}

		// remove any entities that are not allowed in XML 1.0 and replace with
		// "?". In most cases where we use this code, this will not cause any
		// problems.
		xml = INVALID_CHARACTER_ESCAPE_PATTERN.matcher(xml).replaceAll("?");

		try {
			Object result = xstream.fromXML(xml);
			if (result == null) {
				// Leave as is
			} else if (result instanceof List && expectedClass.isArray()) {
				// Got a List but expected an array. Convert on the fly.
				List list = (List) result;
				result = list.toArray((Object[]) Array.newInstance(expectedClass.getComponentType(), list.size()));
			} else if (result instanceof Object[] && List.class.isAssignableFrom(expectedClass)) {
				// Got an array but expected a List. Convert on the fly.
				Object[] array = (Object[]) result;
				result = Arrays.asList(array);
			}
			return expectedClass.cast(result);
		} catch (ClassCastException e) {
			throw new IOException("Invalid class returned during deserialization. Expected " + expectedClass, e);
		} catch (XStreamException e) {
			throw new IOException(e);
		}
	}

	/** An {@link XStream} that also support CDATA encoding. */
	private static class CDataAwareXStream extends XStream {

		/** Attributes that are to be encoded as CDATA. */
		private final Set cdataAttributes;

		/** Constructor. */
		public CDataAwareXStream() {
			this(new HashSet<>());
		}

		/**
		 * Constructor. This separate constructor is a trick to pass the same set to the
		 * object created for the super constructor and also hold a reference to it in
		 * an attribute. If we would construct the set at the declaration point of the
		 * attribute, this could not be passed to super, as super is called before
		 * attributes are initialized.
		 */
		private CDataAwareXStream(Set cdataAttributes) {
			super(new CDataAwareDomDriver(cdataAttributes));
			this.cdataAttributes = cdataAttributes;
			setMode(XStream.NO_REFERENCES);
			autodetectAnnotations(true);
			XStream.setupDefaultSecurity(this);
			allowTypesByWildcard(WHITELISTED_PACKAGES);
			allowTypes(WHITELISTED_CLASSES);
		}

		/**
		 * Converts the given object to XML.
		 *
		 * @param cdataAttributes
		 *            all attributes with given name are converted to CDATA instead of
		 *            "normal" strings.
		 */
		public String toXML(Object object, String[] cdataAttributes) {
			try {
				this.cdataAttributes.addAll(Arrays.asList(cdataAttributes));
				return toXML(object);
			} finally {
				this.cdataAttributes.clear();
			}
		}
	}

	/** A XStream driver that support CDATA output. */
	private static class CDataAwareDomDriver extends DomDriver {

		/** Attributes that are to be encoded as CDATA. */
		private final Set cdataAttributes;

		/** Constructor. */
		public CDataAwareDomDriver(Set cdataAttributes) {
			this.cdataAttributes = cdataAttributes;
		}

		/** {@inheritDoc} */
		@Override
		public HierarchicalStreamWriter createWriter(Writer out) {
			return new CDataAwarePrettyPrintWriter(out, getNameCoder(), cdataAttributes);
		}
	}

	/**
	 * A pretty printer that outputs attributes that contains "JSON" as CDATA.
	 */
	private static class CDataAwarePrettyPrintWriter extends PrettyPrintWriter {

		/** Flag for storing CData mode. */
		private boolean nextIsCData = false;

		/** Attributes that are to be encoded as CDATA. */
		private final Set cdataAttributes;

		/** Constructor. */
		public CDataAwarePrettyPrintWriter(Writer out, NameCoder nameCoder, Set cdataAttributes) {
			super(out, nameCoder);
			this.cdataAttributes = cdataAttributes;
		}

		/** {@inheritDoc} */
		@Override
		public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
			super.startNode(name, clazz);
			nextIsCData = this.cdataAttributes.contains(name);
		}

		/** {@inheritDoc} */
		@Override
		protected void writeText(QuickWriter writer, String text) {
			if (nextIsCData) {
				writer.write("");
			} else {
				super.writeText(writer, text);
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy