org.xflatdb.xflat.query.XPathUpdate.java Source code

Java tutorial

Introduction

Here is the source code for org.xflatdb.xflat.query.XPathUpdate.java

Source

/* 
*   Copyright 2013 Gordon Burgett and individual contributors
*
*   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.xflatdb.xflat.query;

import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xflatdb.xflat.convert.ConversionException;
import org.xflatdb.xflat.convert.ConversionService;
import org.jdom2.Attribute;
import org.jdom2.Content;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Parent;
import org.jdom2.Text;
import org.jdom2.xpath.XPathExpression;

/**
 * Specifies an update operation which sets the value of a matched
 * existing DOM element.
 * The new value must be convertible to {@link Content} or String.
 * @author gordon
 */
public class XPathUpdate {

    private List<Update> updates = new ArrayList<>();

    public List<Update> getUpdates() {
        return updates;
    }

    private ConversionService conversionService;

    /**
     * Sets the conversion service used by this Update operation when it is
     * applied.  The conversion service is used to convert values to JDOM elements
     * and attributes.
     * @param conversionService 
     */
    public void setConversionService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    private XPathUpdate() {

    }

    /**
     * Creates an update that sets the values selected by the XPath expression.
     * The value will only be modified if it exists.  If the XPath expression
     * selects a nonexistent value then no update will be applied.
     * @param path The path selecting an element (or elements) to set.
     * @return an XPath update that sets the given value.
     */
    public static <T> XPathUpdate set(XPathExpression<T> path, Object value) {
        XPathUpdate ret = new XPathUpdate();

        Update<T> u = new Update<>(path, value, UpdateType.SET);
        ret.updates.add(u);

        return ret;
    }

    /**
     * Creates an update that removes the element or attribute selected by the XPath expression.
     * The value will only be deleted if it exists.  If the XPath expression
     * selects a nonexistent value then no update will be applied.
     * @param <T>
     * @param path The path selecting an element (or elements) to set.
     * @return an XPath update that deletes the given value.
     */
    public static <T> XPathUpdate unset(XPathExpression<T> path) {
        XPathUpdate ret = new XPathUpdate();
        ret.updates.add(new Update<>(path, null, UpdateType.UNSET));
        return ret;
    }

    /**
     * Adds an additional update operation that sets the values selected by the XPath expression.
     * The value will only be modified if it exists.  If the XPath expression
     * selects a nonexistent value then no update will be applied.
     * @param path The path selecting an element (or elements) to set.
     * @return an XPath update that sets the given value.
     */
    public <T> XPathUpdate andSet(XPathExpression<T> path, Object value) {
        Update<T> u = new Update<>(path, value, UpdateType.SET);
        this.updates.add(u);

        return this;
    }

    /**
     * Adds an additional update operation that removes the element or attribute selected by the XPath expression.
     * The value will only be modified if it exists.  If the XPath expression
     * selects a nonexistent value then no update will be applied.
     * @param path The path selecting an element (or elements) to set.
     * @return an XPath update that sets the given value.
     */
    public <T> XPathUpdate andUnset(XPathExpression<T> path) {
        this.updates.add(new Update<>(path, null, UpdateType.UNSET));
        return this;
    }

    private static class Update<T> {
        private XPathExpression<T> path;

        public XPathExpression<T> getPath() {
            return path;
        }

        private Object value;

        public Object getValue() {
            return value;
        }

        private UpdateType updateType;

        public UpdateType getUpdateType() {
            return this.updateType;
        }

        private Update(XPathExpression<T> path, Object value, UpdateType type) {
            this.path = path;
            this.value = value;
            this.updateType = type;
        }
    }

    /**
     * Applies the update operations to the given DOM Element representing
     * the data in a selected row.
     * @param rowData The DOM Element representing the data in a selected row.
     * @return true if any updates were applied.
     */
    public int apply(Element rowData) {
        int updateCount = 0;

        for (Update update : this.updates) {
            //the update's value will be one or the other, don't know which
            Content asContent = null;
            String asString = null;

            if (update.value instanceof String) {
                asString = (String) update.value;
            }

            if (update.value instanceof Content) {
                asContent = (Content) update.value;
            }

            for (Object node : update.path.evaluate(rowData)) {
                if (node == null)
                    continue;

                Parent parent;
                Element parentElement;

                if (update.getUpdateType() == UpdateType.UNSET) {
                    if (node instanceof Attribute) {
                        parentElement = ((Attribute) node).getParent();
                        if (parentElement != null) {
                            parentElement.removeAttribute((Attribute) node);
                            updateCount++;
                        }
                    } else if (node instanceof Content) {
                        parent = ((Content) node).getParent();
                        //remove this node from its parent element
                        if (parent != null) {
                            parent.removeContent((Content) node);
                            updateCount++;
                        }
                    }

                    continue;
                }

                //it's a set

                if (node instanceof Attribute) {
                    //for attributes we set the value to empty string
                    //this way it can still be selected by xpath for future updates
                    if (update.value == null) {
                        ((Attribute) node).setValue("");
                        updateCount++;
                    } else {
                        if (asString == null) {
                            asString = getStringValue(update.value);
                        }

                        //if we fail conversion then do nothing.
                        if (asString != null) {
                            ((Attribute) node).setValue(asString);
                            updateCount++;
                        }
                    }

                    continue;
                } else if (!(node instanceof Content)) {
                    //can't do anything
                    continue;
                }

                Content contentNode = (Content) node;

                //need to convert
                if (update.value != null && asContent == null) {
                    asContent = getContentValue(update.value);
                    if (asContent == null) {
                        //failed conversion, try text
                        asString = getStringValue(update.value);
                        if (asString != null) {
                            //success!
                            asContent = new Text(asString);
                        }
                    }
                }

                if (node instanceof Element) {
                    //for elements we also set the value, but the value could be Content
                    if (update.value == null) {
                        ((Element) node).removeContent();
                        updateCount++;
                    } else if (asContent != null) {
                        if (asContent.getParent() != null) {
                            //we used the content before, need to clone it
                            asContent = asContent.clone();
                        }

                        ((Element) node).setContent(asContent);
                        updateCount++;
                    }
                    continue;
                }

                //at this point the node is Text, CDATA or something else.
                //The strategy now is to replace the value in its parent.
                parentElement = contentNode.getParentElement();
                if (parentElement == null) {
                    //can't do anything
                    continue;
                }

                if (update.value == null || asContent != null) {
                    //replace this content in the parent element
                    int index = parentElement.indexOf(contentNode);
                    parentElement.removeContent(index);
                    if (update.value != null) {
                        //if it was null then act like an unset, otherwise
                        //its a replace

                        if (asContent.getParent() != null) {
                            //we used the content before, need to clone it
                            asContent = asContent.clone();
                        }

                        parentElement.addContent(index, asContent);
                    }
                    updateCount++;
                }
            }
        }

        return updateCount;
    }

    private String getStringValue(Object value) {
        if (value == null)
            return null;

        if (value instanceof String)
            return (String) value;

        if (this.conversionService == null || !this.conversionService.canConvert(value.getClass(), String.class)) {
            return null;
        }
        try {
            return this.conversionService.convert(value, String.class);
        } catch (ConversionException ex) {
            Log log = LogFactory.getLog(getClass());
            if (log.isTraceEnabled())
                log.trace("Unable to convert update value to string", ex);
            return null;
        }
    }

    private Content getContentValue(Object value) {
        if (value == null)
            return null;

        if (value instanceof Content)
            return (Content) value;

        if (this.conversionService == null || !this.conversionService.canConvert(value.getClass(), Content.class)) {
            return null;
        }
        try {
            return this.conversionService.convert(value, Content.class);
        } catch (ConversionException ex) {
            Log log = LogFactory.getLog(getClass());
            log.warn("Unable to convert update value to content", ex);
            return null;
        }
    }

    /**
     * Enumerates the different types of updates.
     */
    public enum UpdateType {
        /** An update that sets a value. */
        SET,
        /** An update that deletes a value. */
        UNSET
    }
}