/* (c) 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ /* * Copyright 2006-2007 the original author or authors. * * 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.geoserver.backuprestore.reader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.EndElement; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geoserver.backuprestore.Backup; import org.geoserver.catalog.ValidationResult; import org.geoserver.config.util.XStreamPersister; import org.geoserver.config.util.XStreamPersisterFactory; import org.springframework.batch.core.StepExecution; import org.springframework.batch.item.NonTransientResourceException; import org.springframework.batch.item.UnexpectedInputException; import org.springframework.batch.item.xml.StaxEventItemReader; import org.springframework.batch.item.xml.StaxUtils; import org.springframework.batch.item.xml.stax.DefaultFragmentEventReader; import org.springframework.batch.item.xml.stax.FragmentEventReader; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Item reader for reading XML input based on StAX. * * It extracts fragments from the input XML document which correspond to records for processing. The fragments are wrapped with StartDocument and * EndDocument events so that the fragments can be further processed like standalone XML documents. * * The implementation is <b>not</b> thread-safe. * * Code based on original {@link StaxEventItemReader} by Robert Kasanicky. * * @author Robert Kasanicky * @author Alessio Fabiani, GeoSolutions */ public class CatalogFileReader<T> extends CatalogReader<T> { private static final Log logger = LogFactory.getLog(CatalogFileReader.class); private FragmentEventReader fragmentReader; private XMLEventReader eventReader; private Resource resource; private InputStream inputStream; private List<QName> fragmentRootElementNames; private boolean noInput; private boolean strict = true; public CatalogFileReader(Class<T> clazz, Backup backupFacade, XStreamPersisterFactory xStreamPersisterFactory) { super(clazz, backupFacade, xStreamPersisterFactory); } @Override protected void initialize(StepExecution stepExecution) { if (this.getXp() == null) { setXp(this.xstream.getXStream()); } } /** * In strict mode the reader will throw an exception on {@link #open(org.springframework.batch.item.ExecutionContext)} if the input resource does * not exist. * * @param strict false by default */ public void setStrict(boolean strict) { this.strict = strict; } @Override public void setResource(Resource resource) { this.resource = resource; } /** * @param fragmentRootElementName name of the root element of the fragment */ public void setFragmentRootElementName(String fragmentRootElementName) { setFragmentRootElementNames(new String[] { fragmentRootElementName }); } /** * @param fragmentRootElementNames list of the names of the root element of the fragment */ public void setFragmentRootElementNames(String[] fragmentRootElementNames) { this.fragmentRootElementNames = new ArrayList<QName>(); for (String fragmentRootElementName : fragmentRootElementNames) { this.fragmentRootElementNames .add(parseFragmentRootElementName(fragmentRootElementName)); } } /** * Ensure that all required dependencies for the ItemReader to run are provided after all properties have been set. * * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() * @throws IllegalArgumentException if the Resource, FragmentDeserializer or FragmentRootElementName is null, or if the root element is empty. * @throws IllegalStateException if the Resource does not exist. */ @Override public void afterPropertiesSet() throws Exception { Assert.notEmpty(fragmentRootElementNames, "The FragmentRootElementNames must not be empty"); for (QName fragmentRootElementName : fragmentRootElementNames) { Assert.hasText(fragmentRootElementName.getLocalPart(), "The FragmentRootElementNames must not contain empty elements"); } } /** * Responsible for moving the cursor before the StartElement of the fragment root. * * This implementation simply looks for the next corresponding element, it does not care about element nesting. You will need to override this * method to correctly handle composite fragments. * * @return <code>true</code> if next fragment was found, <code>false</code> otherwise. * * @throws NonTransientResourceException if the cursor could not be moved. This will be treated as fatal and subsequent calls to read will return * null. */ protected boolean moveCursorToNextFragment(XMLEventReader reader) { try { while (true) { while (reader.peek() != null && !reader.peek().isStartElement()) { reader.nextEvent(); } if (reader.peek() == null) { return false; } QName startElementName = ((StartElement) reader.peek()).getName(); if (isFragmentRootElementName(startElementName)) { return true; } reader.nextEvent(); } } catch (XMLStreamException e) { return logValidationExceptions((T) null, new NonTransientResourceException("Error while reading from event reader", e)); } } @Override protected void doClose() throws Exception { try { if (fragmentReader != null) { fragmentReader.close(); } if (inputStream != null) { inputStream.close(); } } catch (IOException | XMLStreamException e) { logValidationExceptions((T) null, e); } finally { fragmentReader = null; inputStream = null; } } @Override protected void doOpen() throws Exception { Assert.notNull(resource, "The Resource must not be null."); try { noInput = true; if (!resource.exists()) { if (strict) { throw new IllegalStateException( "Input resource must exist (reader is in 'strict' mode)"); } logger.warn("Input resource does not exist " + resource.getDescription()); return; } if (!resource.isReadable()) { if (strict) { throw new IllegalStateException( "Input resource must be readable (reader is in 'strict' mode)"); } logger.warn("Input resource is not readable " + resource.getDescription()); return; } inputStream = resource.getInputStream(); eventReader = XMLInputFactory.newInstance().createXMLEventReader(inputStream); fragmentReader = new DefaultFragmentEventReader(eventReader); noInput = false; } catch (Exception e) { logValidationExceptions((T) null, e); } } /** * Move to next fragment and map it to item. */ @Override protected T doRead() throws Exception { T item = null; try { if (noInput) { return null; } boolean success = false; try { success = moveCursorToNextFragment(fragmentReader); } catch (NonTransientResourceException e) { // Prevent caller from retrying indefinitely since this is fatal noInput = true; throw e; } if (success) { fragmentReader.markStartFragment(); try { @SuppressWarnings("unchecked") T mappedFragment = (T) unmarshal(StaxUtils.getSource(fragmentReader)); item = mappedFragment; try { firePostRead(item, resource); } catch (IOException e) { logValidationExceptions((ValidationResult) null, new UnexpectedInputException( "Could not write data. The file may be corrupt.", e)); } } finally { fragmentReader.markFragmentProcessed(); } } } catch (Exception e) { logValidationExceptions((T) null, e); } return item; } /** * This is the core of the Class. The XML fragment will be unmarshalled via the GeoServer {@link XStreamPersister}. * * @param source * @return * @throws TransformerException * @throws XMLStreamException */ private Object unmarshal(Source source) throws TransformerException, XMLStreamException { TransformerFactory tf = new com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl(); Transformer t = tf.newTransformer(); ByteArrayOutputStream os = new ByteArrayOutputStream(); Result result = new StreamResult(os); t.transform(source, result); return this.getXp().fromXML(new ByteArrayInputStream(os.toByteArray())); } /* * jumpToItem is overridden because reading in and attempting to bind an entire fragment is unacceptable in a restart scenario, and may cause * exceptions to be thrown that were already skipped in previous runs. */ @Override protected void jumpToItem(int itemIndex) throws Exception { for (int i = 0; i < itemIndex; i++) { try { QName fragmentName = readToStartFragment(); readToEndFragment(fragmentName); } catch (NoSuchElementException e) { if (itemIndex == (i + 1)) { // we can presume a NoSuchElementException on the last item means the EOF was reached on the last run return; } else { // if NoSuchElementException occurs on an item other than the last one, this indicates a problem logValidationExceptions((T) null, e); } } } } /* * Read until the first StartElement tag that matches any of the provided fragmentRootElementNames. Because there may be any number of tags in * between where the reader is now and the fragment start, this is done in a loop until the element type and name match. */ private QName readToStartFragment() throws XMLStreamException { while (true) { XMLEvent nextEvent = eventReader.nextEvent(); if (nextEvent.isStartElement() && isFragmentRootElementName(((StartElement) nextEvent).getName())) { return ((StartElement) nextEvent).getName(); } } } /* * Read until the first EndElement tag that matches the provided fragmentRootElementName. Because there may be any number of tags in between where * the reader is now and the fragment end tag, this is done in a loop until the element type and name match */ private void readToEndFragment(QName fragmentRootElementName) throws XMLStreamException { while (true) { XMLEvent nextEvent = eventReader.nextEvent(); if (nextEvent.isEndElement() && fragmentRootElementName.equals(((EndElement) nextEvent).getName())) { return; } } } private boolean isFragmentRootElementName(QName name) { for (QName fragmentRootElementName : fragmentRootElementNames) { if (fragmentRootElementName.getLocalPart().equals(name.getLocalPart())) { if (!StringUtils.hasText(fragmentRootElementName.getNamespaceURI()) || fragmentRootElementName.getNamespaceURI() .equals(name.getNamespaceURI())) { return true; } } } return false; } private QName parseFragmentRootElementName(String fragmentRootElementName) { String name = fragmentRootElementName; String nameSpace = null; if (fragmentRootElementName.contains("{")) { nameSpace = fragmentRootElementName.replaceAll("\\{(.*)\\}.*", "$1"); name = fragmentRootElementName.replaceAll("\\{.*\\}(.*)", "$1"); } return new QName(nameSpace, name, ""); } }