/* * Copyright 2017 ThoughtWorks, Inc. * * 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 com.thoughtworks.go.config; import com.thoughtworks.go.config.exceptions.GoConfigInvalidException; import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.util.GoConstants; import com.thoughtworks.go.util.XmlUtils; import org.apache.log4j.Logger; import org.jdom2.*; import java.io.*; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static com.thoughtworks.go.config.ConfigCache.annotationFor; import static com.thoughtworks.go.config.ConfigCache.isAnnotationPresent; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIf; import static com.thoughtworks.go.util.ObjectUtil.nullSafeEquals; import static com.thoughtworks.go.util.XmlUtils.buildXmlDocument; import static java.text.MessageFormat.format; public class MagicalGoConfigXmlWriter { private static final Logger LOGGER = Logger.getLogger(MagicalGoConfigXmlWriter.class); public static final String XML_NS = "http://www.w3.org/2001/XMLSchema-instance"; private ConfigCache configCache; private final ConfigElementImplementationRegistry registry; public MagicalGoConfigXmlWriter(ConfigCache configCache, ConfigElementImplementationRegistry registry) { this.configCache = configCache; this.registry = registry; } private Document createEmptyCruiseConfigDocument() { Element root = new Element("cruise"); Namespace xsiNamespace = Namespace.getNamespace("xsi", XML_NS); root.addNamespaceDeclaration(xsiNamespace); registry.registerNamespacesInto(root); root.setAttribute("noNamespaceSchemaLocation", "cruise-config.xsd", xsiNamespace); String xsds = registry.xsds(); if (!xsds.isEmpty()) { root.setAttribute("schemaLocation", xsds, xsiNamespace); } root.setAttribute("schemaVersion", Integer.toString(GoConstants.CONFIG_SCHEMA_VERSION)); return new Document(root); } public void write(CruiseConfig configForEdit, OutputStream output, boolean skipPreprocessingAndValidation) throws Exception { LOGGER.debug("[Serializing Config] Starting to write. Validation skipped? " + skipPreprocessingAndValidation); MagicalGoConfigXmlLoader loader = new MagicalGoConfigXmlLoader(configCache, registry); if (!configForEdit.getOrigin().isLocal()) { throw new GoConfigInvalidException(configForEdit,"Attempted to save merged configuration with patials"); } if (!skipPreprocessingAndValidation) { loader.preprocessAndValidate(configForEdit); LOGGER.debug("[Serializing Config] Done with cruise config validators."); } Document document = createEmptyCruiseConfigDocument(); write(configForEdit, document.getRootElement(), configCache, registry); LOGGER.debug("[Serializing Config] XSD and DOM validation."); verifyXsdValid(document); MagicalGoConfigXmlLoader.validateDom(document.getRootElement(), registry); LOGGER.info("[Serializing Config] Generating config partial."); XmlUtils.writeXml(document, output); LOGGER.debug("[Serializing Config] Finished writing config partial."); } public Document documentFrom(CruiseConfig config) { Document document = createEmptyCruiseConfigDocument(); write(config, document.getRootElement(), configCache, registry); return document; } public String toString(Document document) throws IOException { org.apache.commons.io.output.ByteArrayOutputStream outputStream = new org.apache.commons.io.output.ByteArrayOutputStream(); XmlUtils.writeXml(document, outputStream); return outputStream.toString(StandardCharsets.UTF_8); } public void verifyXsdValid(Document document) throws Exception { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); XmlUtils.writeXml(document, buffer); InputStream content = new ByteArrayInputStream(buffer.toByteArray()); buildXmlDocument(content, GoConfigSchema.getCurrentSchema(), registry.xsds()); } public String toXmlPartial(Object domainObject) { bombIf(!isAnnotationPresent(domainObject.getClass(), ConfigTag.class), "Object " + domainObject + " does not have a ConfigTag"); Element element = elementFor(domainObject.getClass(), configCache); write(domainObject, element, configCache, registry); if (isAnnotationPresent(domainObject.getClass(), ConfigCollection.class) && domainObject instanceof Collection) { for (Object item : (Collection) domainObject) { if (isAnnotationPresent(item.getClass(), ConfigCollection.class) && item instanceof Collection) { new ExplicitCollectionXmlFieldWithValue(domainObject.getClass(), null, (Collection) item, configCache, registry).populate(element); continue; } Element childElement = elementFor(item.getClass(), configCache); element.addContent(childElement); write(item, childElement, configCache, registry); } } try { ByteArrayOutputStream output = new ByteArrayOutputStream(); XmlUtils.writeXml(element, output); return output.toString(); } catch (IOException e) { throw bomb("Unable to write xml to String"); } } private static Namespace namespaceFor(ConfigTag annotation) { return Namespace.getNamespace(annotation.namespacePrefix(), annotation.namespaceURI()); } private static void write(Object o, Element element, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { for (XmlFieldWithValue xmlFieldWithValue : allFields(o, configCache, registry)) { if (xmlFieldWithValue.isDefault() && !xmlFieldWithValue.alwaysWrite()) { continue; } xmlFieldWithValue.populate(element); } } private static List<XmlFieldWithValue> allFields(Object o, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { List<XmlFieldWithValue> list = new ArrayList<>(); Class originalClass = o.getClass(); for (GoConfigFieldWriter field : allFieldsWithInherited(originalClass, o, configCache, registry)) { Field configField = field.getConfigField(); if (field.isImplicitCollection()) { list.add(new ImplicitCollectionXmlFieldWithValue(originalClass, configField, (Collection) field.getValue(), configCache, registry)); } else if (field.isConfigCollection()) { list.add(new ExplicitCollectionXmlFieldWithValue(originalClass, configField, (Collection) field.getValue(), configCache, registry)); } else if (field.isSubtag()) { list.add(new SubTagXmlFieldWithValue(originalClass, configField, field.getValue(), configCache, registry)); } else if (field.isAttribute()) { final Object value = field.getValue(); list.add(new AttributeXmlFieldWithValue(originalClass, configField, value, configCache, registry)); } else if (field.isConfigValue()) { list.add(new ValueXmlFieldWithValue(configField, field.getValue(), originalClass, configCache, registry)); } } return list; } private static List<GoConfigFieldWriter> allFieldsWithInherited(Class aClass, Object o, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { return new GoConfigClassWriter(aClass, configCache, registry).getAllFields(o); } private abstract static class XmlFieldWithValue<T> { protected final Field field; protected final Class originalClass; protected final T value; protected final ConfigCache configCache; protected final ConfigElementImplementationRegistry registry; private XmlFieldWithValue(Class originalClass, Field field, T value, ConfigCache configCache, ConfigElementImplementationRegistry registry) { this.originalClass = originalClass; this.value = value; this.field = field; this.configCache = configCache; this.registry = registry; } public boolean isDefault() { try { Object defaultObject = ConfigElementInstantiator.instantiateConfigElement(new GoCipher(), originalClass); Object defaultValue = field.get(defaultObject); return nullSafeEquals(value, defaultValue); } catch (Exception e) { return false; } } public abstract void populate(Element parent); public abstract boolean alwaysWrite(); protected String valueString() { String valueString = null; ConfigAttributeValue attributeValue = value.getClass().getAnnotation(ConfigAttributeValue.class); if (attributeValue != null) { try { Field field = getField(value.getClass(), attributeValue); field.setAccessible(true); valueString = field.get(value).toString(); } catch (NoSuchFieldException e) { //noinspection ThrowableResultOfMethodCallIgnored bomb(e); } catch (IllegalAccessException e) { //noinspection ThrowableResultOfMethodCallIgnored bomb(e); } } else { valueString = value.toString(); } return valueString; } private Field getField(Class clazz, ConfigAttributeValue attributeValue) throws NoSuchFieldException { try { return clazz.getDeclaredField(attributeValue.fieldName()); } catch (NoSuchFieldException e) { Class klass = clazz.getSuperclass(); if (klass == null) { throw e; } return getField(klass, attributeValue); } } } private static Element elementFor(Class<?> aClass, ConfigCache configCache) { ConfigTag configTag = annotationFor(aClass, ConfigTag.class); if(configTag == null) throw bomb(format("Cannot get config tag for {0}",aClass)); return new Element(configTag.value(), namespaceFor(configTag)); } private static class SubTagXmlFieldWithValue extends XmlFieldWithValue<Object> { public SubTagXmlFieldWithValue(Class oringinalClass, Field field, Object value, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { super(oringinalClass, field, value, configCache, registry); } public void populate(Element parent) { Element child = elementFor(value.getClass(), configCache); parent.addContent(child); write(value, child, configCache, registry); } public boolean alwaysWrite() { return false; } } private static class AttributeXmlFieldWithValue extends XmlFieldWithValue<Object> { public AttributeXmlFieldWithValue(Class oringinalClass, Field field, Object current, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { super(oringinalClass, field, current, configCache, registry); } public void populate(Element parent) { if (value == null && !isDefault()) { if (!isDefault()) { throw bomb( format("Try to write null value into configuration! [{0}.{1}]", field.getDeclaringClass().getName(), field.getName())); } throw bomb(format("A non default field {0}(on {1}) had null value", field.getName(), field.getDeclaringClass().getName())); } String attributeName = field.getAnnotation(ConfigAttribute.class).value(); parent.setAttribute(new Attribute(attributeName, valueString())); } public boolean alwaysWrite() { return field.getAnnotation(ConfigAttribute.class).alwaysWrite(); } } private static class ImplicitCollectionXmlFieldWithValue extends XmlFieldWithValue<Collection> { public ImplicitCollectionXmlFieldWithValue( Class oringinalClass, Field field, Collection value, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { super(oringinalClass, field, value, configCache, registry); } public void populate(Element parent) { new CollectionXmlFieldWithValue(value, parent, originalClass, configCache, registry).populate(); } public boolean alwaysWrite() { return false; } } private static class CollectionXmlFieldWithValue { private final Collection value; private final Element parent; private final Class originalClass; private final ConfigCache configCache; private final ConfigElementImplementationRegistry registry; public CollectionXmlFieldWithValue(Collection value, Element parent, Class originalClass, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { this.value = value; this.parent = parent; this.originalClass = originalClass; this.configCache = configCache; this.registry = registry; } public void populate() { Collection defaultCollection = generateDefaultCollection(); for (XmlFieldWithValue xmlFieldWithValue : allFields(value, configCache, registry)) { if (!xmlFieldWithValue.isDefault()) { xmlFieldWithValue.populate(parent); } } for (Object item : value) { if (defaultCollection.contains(item)) { continue; } if (item.getClass().isAnnotationPresent(ConfigCollection.class) && item instanceof Collection) { new ExplicitCollectionXmlFieldWithValue(originalClass, null, (Collection) item, configCache, registry).populate(parent); continue; } Element childElement = elementFor(item.getClass(), configCache); parent.addContent(childElement); write(item, childElement, configCache, registry); } } protected Collection generateDefaultCollection() { Class<? extends Collection> clazz = value.getClass(); try { return clazz.newInstance(); } catch (Exception e) { throw bomb("Error creating default instance of " + clazz.getName(), e); } } } private static class ExplicitCollectionXmlFieldWithValue extends XmlFieldWithValue<Collection> { public ExplicitCollectionXmlFieldWithValue(Class oringinalClass, Field field, Collection value, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { super(oringinalClass, field, value, configCache, registry); } public void populate(Element parent) { Element containerElement = elementFor(value.getClass(), configCache); new CollectionXmlFieldWithValue(value, containerElement, originalClass, configCache, registry).populate(); parent.addContent(containerElement); } public boolean alwaysWrite() { return false; } } private static class ValueXmlFieldWithValue extends XmlFieldWithValue<Object> { private boolean requireCdata; public ValueXmlFieldWithValue(Field field, Object value, Class oringinalClass, ConfigCache configCache, final ConfigElementImplementationRegistry registry) { super(oringinalClass, field, value, configCache, registry); ConfigValue configValue = field.getAnnotation(ConfigValue.class); requireCdata = configValue.requireCdata(); } public void populate(Element parent) { if (requireCdata) { parent.addContent(new CDATA(valueString())); } else { parent.setText(valueString()); } } public boolean alwaysWrite() { return false; } } }