org.alloy.metal.xml.merge.MergeContext.java Source code

Java tutorial

Introduction

Here is the source code for org.alloy.metal.xml.merge.MergeContext.java

Source

/*
* #%L
* BroadleafCommerce Common Libraries
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* 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.
* #L%
*/

package org.alloy.metal.xml.merge;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.alloy.metal.collections.list.MutableList;
import org.alloy.metal.collections.list._Lists;
import org.alloy.metal.configuration.ConfigurationLocation;
import org.alloy.metal.configuration.PatchableConfiguration;
import org.alloy.metal.configuration._Configuration;
import org.alloy.metal.resource.ResourceInputStream;
import org.alloy.metal.spring._ApplicationResource;
import org.alloy.metal.string._String;
import org.alloy.metal.xml.merge.handlers.MergeHandler;
import org.alloy.metal.xml.merge.handlers.MergeMatcherType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.google.common.collect.Lists;

/**
* This class manages all xml merge interactions with callers. It is responsible for
* not only loading the handler configurations, but also for cycling through the handlers
* in a prioritized fashion and exporting the final merged document.
*
* @author jfischer
*
*/
public class MergeContext implements PatchableConfiguration, ApplicationContextAware {
    private static final Log LOG = LogFactory.getLog(MergeContext.class);
    private static DocumentBuilder builder;

    static {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            builder = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            LOG.error("Unable to create document builder", e);
            throw new RuntimeException(e);
        }
    }

    private ApplicationContext applicationContext;
    private String defaultHandlerConfiguration;
    private List<MergeHandler> handlers = Lists.newArrayList();
    private List<ConfigurationLocation> patchLocations = Lists.newArrayList();

    public MergeContext(String defaultHandlerConfiguration, ApplicationContext applicationContext)
            throws MergeManagerSetupException {
        this.defaultHandlerConfiguration = defaultHandlerConfiguration;
        this.applicationContext = applicationContext;

        try {
            Properties props = loadProperties();
            removeSkippedMergeComponents(props);
            setHandlers(props);
        } catch (IOException e) {
            throw new MergeManagerSetupException(e);
        } catch (ClassNotFoundException e) {
            throw new MergeManagerSetupException(e);
        } catch (IllegalAccessException e) {
            throw new MergeManagerSetupException(e);
        } catch (InstantiationException e) {
            throw new MergeManagerSetupException(e);
        }
    }

    private void removeSkippedMergeComponents(Properties props) {
        InputStream inputStream = this.getClass().getClassLoader()
                .getResourceAsStream("/broadleaf-commmerce/skipMergeComponents.txt");

        if (inputStream != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("mergeClassOverrides file found.");
            }

            final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            try {
                while (bufferedReader.ready()) {
                    String line = bufferedReader.readLine();
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("mergeComponentOverrides - overridding " + line);
                    }
                    removeSkipMergeComponents(props, line);
                }
            } catch (IOException e) {
                LOG.error("Error reading resource - /broadleaf-commmerce/skipMergeComponents.txt", e);
            } finally {
                try {
                    bufferedReader.close();
                } catch (IOException ioe) {
                    LOG.error("Error closing resource - /broadleaf-commmerce/skipMergeComponents.txt", ioe);
                }
            }
        }
    }

    /**
    * Examines the properties file for an entry with an id equal to the component that we want
    * to ignore and then removes all keys that have the same number (e.g. if xpath.28 is the key
    * then handler.28, xpath.28, and priority.28 will all be removed).
    *
    * @param props
    * @param componentName
    */
    private void removeSkipMergeComponents(Properties props, String componentName) {
        String lookupName = "@id='" + componentName.trim() + "'";
        String key = findComponentKey(lookupName, props);
        while (key != null) {
            removeItemsMatchingKey(key, props);
            key = findComponentKey(lookupName, props);
        }
    }

    /**
    * Examines the properties file for an entry that contains the passed in component id string and returns its key
    *
    * to ignore.
    *
    * @param componentName
    * @param props
    * @return
    */
    private String findComponentKey(String componentIdStr, Properties props) {
        for (Map.Entry<Object, Object> entry : props.entrySet()) {
            Object value = entry.getValue();
            if (value instanceof String) {
                String valueStr = (String) value;
                if (valueStr.contains(componentIdStr)) {
                    Object key = entry.getKey();
                    if (key instanceof String) {
                        return (String) key;
                    }
                }
            }
        }
        return null;
    }

    /**
    * Removes all keys that share the same number. (e.g. if xpath.28 is the key
    * then handler.28, xpath.28, and priority.28 will all be removed).
    *
    * @param firstKey
    * @param props
    * @return
    */
    private void removeItemsMatchingKey(String firstKey, Properties props) {
        int dotPos = firstKey.indexOf(".");
        if (dotPos > 0) {
            String keyNumberToMatch = firstKey.substring(dotPos);

            Iterator<Object> iter = props.keySet().iterator();

            while (iter.hasNext()) {
                Object keyObj = iter.next();
                if (keyObj instanceof String) {
                    String keyStr = (String) keyObj;
                    dotPos = keyStr.indexOf(".");
                    String keyNumber = keyStr.substring(dotPos);
                    if (keyNumber.equals(keyNumberToMatch)) {
                        iter.remove();
                    }
                }
            }
        }
    }

    /**
    * Merge 2 xml document streams together into a final resulting stream. During
    * the merge, various merge business rules are followed based on configuration
    * defined for various merge points.
    *
    * @param stream1
    * @param stream2
    * @return the stream representing the merged document
    * @throws org.broadleafcommerce.common.extensibility.context.merge.exceptions.MergeException
    */
    public ResourceInputStream merge(ResourceInputStream stream1, ResourceInputStream stream2)
            throws MergeException {
        try {
            Document doc1 = builder.parse(stream1);
            Document doc2 = builder.parse(stream2);

            List<Node> exhaustedNodes = new ArrayList<Node>();

            // process any defined handlers
            for (MergeHandler handler : this.handlers) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Processing handler: " + handler.getXPath());
                }
                MergePoint point = new MergePoint(handler, doc1, doc2);
                List<Node> list = point.merge(exhaustedNodes);
                exhaustedNodes.addAll(list);
            }

            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer xmlTransformer = tFactory.newTransformer();
            xmlTransformer.setOutputProperty(OutputKeys.VERSION, "1.0");
            xmlTransformer.setOutputProperty(OutputKeys.ENCODING, _String.CHARACTER_ENCODING.toString());
            xmlTransformer.setOutputProperty(OutputKeys.METHOD, "xml");
            xmlTransformer.setOutputProperty(OutputKeys.INDENT, "yes");

            if (doc1.getDoctype() != null && doc1.getDoctype().getSystemId() != null) {
                xmlTransformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc1.getDoctype().getSystemId());
            }

            DOMSource source = new DOMSource(doc1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(baos));
            StreamResult result = new StreamResult(writer);
            xmlTransformer.transform(source, result);

            byte[] itemArray = baos.toByteArray();

            return new ResourceInputStream(new ByteArrayInputStream(itemArray), stream2.getName(),
                    stream1.getNames());
        } catch (Exception e) {
            throw new MergeException(e);
        }
    }

    private void setHandlers(Properties props)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MutableList<MergeHandler> tempHandlers = _Lists.list();
        String[] keys = props.keySet().toArray(new String[props.keySet().size()]);
        for (String key : keys) {
            if (key.startsWith("handler.")) {
                MergeHandler temp = (MergeHandler) Class.forName(props.getProperty(key)).newInstance();
                String name = key.substring(8, key.length());
                temp.setName(name);

                String priority = props.getProperty("priority." + name);
                if (priority != null) {
                    temp.setPriority(Integer.parseInt(priority));
                }

                String xpath = props.getProperty("xpath." + name);
                if (xpath != null) {
                    temp.setXPath(xpath);
                }

                String matcherType = props.getProperty("matcherType." + name);
                if (matcherType != null) {
                    temp.setMatcherType(MergeMatcherType.valueOf(matcherType));
                }

                tempHandlers.add(temp);
            }
        }

        List<MergeHandler> finalHandlers = Lists.newArrayList();
        for (MergeHandler temp : tempHandlers) {
            if (temp.getName().contains(".")) {
                String parentName = temp.getName().substring(0, temp.getName().lastIndexOf("."));

                MergeHandler parent = tempHandlers.filter((handler) -> handler.getName().equals(parentName))
                        .singleStrict();

                parent.getChildren().add(temp);
            } else {
                finalHandlers.add(temp);
            }
        }

        this.handlers = finalHandlers;
    }

    private Properties loadProperties() throws IOException {
        Properties props = new Properties();
        props.load(_ApplicationResource.getResource(this.defaultHandlerConfiguration, this.applicationContext)
                .getInputStream());

        List<ResourceInputStream> configurations = _Configuration.getConfigurations(this.patchLocations,
                this.applicationContext);
        for (ResourceInputStream configuration : configurations) {
            props.load(configuration);
        }

        return props;
    }

    public String serialize(InputStream in) {
        InputStreamReader reader = null;
        int temp;
        StringBuilder item = new StringBuilder();
        boolean eof = false;
        try {
            reader = new InputStreamReader(in);
            while (!eof) {
                temp = reader.read();
                if (temp == -1) {
                    eof = true;
                } else {
                    item.append((char) temp);
                }
            }
        } catch (IOException e) {
            LOG.error("Unable to merge source and patch locations", e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (Throwable e) {
                    LOG.error("Unable to merge source and patch locations", e);
                }
            }
        }

        return item.toString();
    }

    @Override
    public List<ConfigurationLocation> getPatchLocations() {
        return patchLocations;
    }

    @Override
    public void setPatchLocations(List<ConfigurationLocation> patchLocations) {
        this.patchLocations = patchLocations;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}