Java tutorial
/* * 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 } }