Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXReader.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXReader.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXReader.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXReader.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,734 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import org.apache.sis.internal.xml.StaxStreamReader; +import com.esri.core.geometry.Point; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.ArrayList; +import java.util.List; +import javax.xml.stream.XMLStreamException; +import org.opengis.geometry.Envelope; +import org.opengis.feature.Feature; + +import static javax.xml.stream.XMLStreamReader.*; +import org.apache.sis.geometry.ImmutableEnvelope; +import static org.apache.sis.internal.gpx.GPXConstants.*; +import org.apache.sis.referencing.CommonCRS; + +/** + * Stax reader class for GPX 1.0 and 1.1 files. + * + * Usage :<br> + * <pre> + * {@code + * final GPXReader reader = new GPXReader(); + * reader.setInput(gpxInput); + * + * final GPXVersion version = reader.getVersion(); + * final Metadata metadata = reader.getMetadata(); + * + * while(reader.hasNext()) { + * Feature feature = reader.next(); + * } + * + * } + * </pre> + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public class GPXReader extends StaxStreamReader { + + private MetaData metadata; + private Feature current; + private int wayPointInc = 0; + private int routeInc = 0; + private int trackInc = 0; + private GPXVersion version = null; + private String baseNamespace = GPX_NAMESPACE_V11; + + /** + * {@inheritDoc } + * + * @param input input object + * @throws IOException if input failed to be opened for any IO reason + * @throws XMLStreamException if input is not a valid XML stream + */ + @Override + public void setInput(final Object input) throws IOException, XMLStreamException { + super.setInput(input); + + //search for the bound tag to generate the envelope + searchLoop : + while(reader.hasNext()){ + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String typeName = reader.getLocalName(); + if(TAG_GPX.equalsIgnoreCase(typeName)){ + + String str = "1.1"; //consider 1.1 by default + for(int i=0,n=reader.getAttributeCount(); i<n;i++){ + if(ATT_GPX_VERSION.equalsIgnoreCase(reader.getAttributeLocalName(i))){ + str = reader.getAttributeValue(i); + } + } + + try{ + this.version = GPXVersion.toVersion(str); + }catch(NumberFormatException ex){ + throw new XMLStreamException(ex); + } + + if(version == GPXVersion.v1_0_0){ + baseNamespace = GPX_NAMESPACE_V10; + //we wont found a metadata tag, must read the tags here. + metadata = parseMetaData100(); + break searchLoop; + }else{ + baseNamespace = GPX_NAMESPACE_V11; + } + + }else if(TAG_METADATA.equalsIgnoreCase(typeName)){ + metadata = parseMetaData110(); + break searchLoop; + }else if( TAG_WPT.equalsIgnoreCase(typeName) + || TAG_TRK.equalsIgnoreCase(typeName) + || TAG_RTE.equalsIgnoreCase(typeName)){ + //there is no metadata tag + break searchLoop; + } + } + } + + } + + /** + * Get GPX file version. + * This method will return a result only if called only after the input has been set. + * + * @return GPXVersion or null if input is not set. + */ + public GPXVersion getVersion() { + return version; + } + + /** + * Get GPX metadata. + * This method will return a result only if called only after the input has been set. + * + * @return Metadata or null if input is not set. + */ + public MetaData getMetadata() { + return metadata; + } + + /** + * {@inheritDoc } + */ + @Override + public void reset() throws IOException, XMLStreamException { + super.reset(); + metadata = null; + current = null; + wayPointInc = 0; + routeInc = 0; + trackInc = 0; + } + + /** + * Returns true if there is a next feature in the stream. + * + * @return true if there is next feature + * @throws javax.xml.stream.XMLStreamException if xml parser encounter an invalid + * element or underlying stream caused an exception + */ + public boolean hasNext() throws XMLStreamException { + findNext(); + return current != null; + } + + /** + * Get next feature in the stream. + * + * @return GPX WayPoint, Route or track + * @throws javax.xml.stream.XMLStreamException if xml parser encounter an invalid + * element or underlying stream caused an exception + */ + public Feature next() throws XMLStreamException { + findNext(); + final Feature ele = current; + current = null; + return ele; + } + + /** + * Search for the next feature in the stax stream. + * This method will set the current local property if there is one. + */ + private void findNext() throws XMLStreamException { + if(current != null) return; + + boolean first = true; + while ( first || (current == null && reader.hasNext()) ) { + final int type; + if(first){ + type = reader.getEventType(); + first = false; + }else{ + type = reader.next(); + } + + if(type == START_ELEMENT) { + final String localName = reader.getLocalName(); + if(TAG_WPT.equalsIgnoreCase(localName)){ + current = parseWayPoint(wayPointInc++); + break; + }else if(TAG_RTE.equalsIgnoreCase(localName)){ + current = parseRoute(routeInc++); + break; + }else if(TAG_TRK.equalsIgnoreCase(localName)){ + current = parseTrack(trackInc++); + break; + } + } + } + + } + + /** + * Parse current metadata element. + * The stax reader must be placed to the start element of the metadata. + * + * @return MetaData + */ + private MetaData parseMetaData100() throws XMLStreamException { + + final MetaData metadata = new MetaData(); + + searchLoop: + while (reader.hasNext()) { + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_NAME.equalsIgnoreCase(localName)){ + metadata.setName(reader.getElementText()); + }else if(TAG_DESC.equalsIgnoreCase(localName)){ + metadata.setDescription(reader.getElementText()); + }else if(TAG_AUTHOR.equalsIgnoreCase(localName)){ + if(metadata.getPerson()==null) metadata.setPerson(new Person()); + metadata.getPerson().setName(reader.getElementText()); + }else if(TAG_AUTHOR_EMAIL.equalsIgnoreCase(localName)){ + if(metadata.getPerson()==null) metadata.setPerson(new Person()); + metadata.getPerson().setEmail(reader.getElementText()); + }else if(TAG_URL.equalsIgnoreCase(localName)){ + try { + metadata.getLinks().add(new URI(reader.getElementText())); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + }else if(TAG_URLNAME.equalsIgnoreCase(localName)){ + //reader.getElementText(); + }else if(TAG_METADATA_TIME.equalsIgnoreCase(localName)){ + metadata.setTime(parseTime(reader.getElementText())); + }else if(TAG_METADATA_KEYWORDS.equalsIgnoreCase(localName)){ + metadata.setKeywords(reader.getElementText()); + }else if(TAG_BOUNDS.equalsIgnoreCase(localName)){ + metadata.setBounds(parseBound()); + }else if( TAG_WPT.equalsIgnoreCase(localName) + || TAG_TRK.equalsIgnoreCase(localName) + || TAG_RTE.equalsIgnoreCase(localName)){ + //there is no more metadata tags + break searchLoop; + } + break; + } + } + + return metadata; + } + + /** + * Parse current metadata element. + * The stax reader must be placed to the start element of the metadata. + * + * @return MetaData + */ + private MetaData parseMetaData110() throws XMLStreamException { + + final MetaData metadata = new MetaData(); + + while (reader.hasNext()) { + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_NAME.equalsIgnoreCase(localName)){ + metadata.setName(reader.getElementText()); + }else if(TAG_DESC.equalsIgnoreCase(localName)){ + metadata.setDescription(reader.getElementText()); + }else if(TAG_AUTHOR.equalsIgnoreCase(localName)){ + metadata.setPerson(parsePerson()); + }else if(TAG_COPYRIGHT.equalsIgnoreCase(localName)){ + metadata.setCopyRight(parseCopyRight()); + }else if(TAG_LINK.equalsIgnoreCase(localName)){ + metadata.getLinks().add(parseLink()); + }else if(TAG_METADATA_TIME.equalsIgnoreCase(localName)){ + metadata.setTime(parseTime(reader.getElementText())); + }else if(TAG_METADATA_KEYWORDS.equalsIgnoreCase(localName)){ + metadata.setKeywords(reader.getElementText()); + }else if(TAG_BOUNDS.equalsIgnoreCase(localName)){ + metadata.setBounds(parseBound()); + } + break; + case END_ELEMENT: + if(TAG_METADATA.equalsIgnoreCase(reader.getLocalName())){ + //end of the metadata element + return metadata; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, relation tag without end."); + } + + /** + * Parse current copyright element. + * The stax reader must be placed to the start element of the copyright. + * + * @return CopyRight + */ + private CopyRight parseCopyRight() throws XMLStreamException { + + final CopyRight copyright = new CopyRight(); + copyright.setAuthor(reader.getAttributeValue(null, ATT_COPYRIGHT_AUTHOR)); + + while (reader.hasNext()) { + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_COPYRIGHT_YEAR.equalsIgnoreCase(localName)){ + copyright.setYear(Integer.valueOf(reader.getElementText())); + }else if(TAG_COPYRIGHT_LICENSE.equalsIgnoreCase(localName)){ + try { + copyright.setLicense( new URI(reader.getElementText())); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + } + break; + case END_ELEMENT: + if(TAG_COPYRIGHT.equalsIgnoreCase(reader.getLocalName())){ + return copyright; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, copyright tag without end."); + } + + /** + * Parse current URI element. + * The stax reader must be placed to the start element. + * + * @return URI + */ + private URI parseLink() throws XMLStreamException { + + String text = reader.getAttributeValue(null, ATT_LINK_HREF); + String mime = null; + + while (reader.hasNext()) { + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_LINK_TEXT.equalsIgnoreCase(localName) && text==null){ + text = reader.getElementText(); + }else if(TAG_LINK_TYPE.equalsIgnoreCase(localName)){ + mime = reader.getElementText(); + } + break; + case END_ELEMENT: + if(TAG_LINK.equalsIgnoreCase(reader.getLocalName())){ + try { + //end of the link element + return new URI(text); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + } + break; + } + } + + throw new XMLStreamException("Error in xml file, link tag without end."); + } + + /** + * Parse current Person element. + * The stax reader must be placed to the start element. + * + * @return Person + */ + private Person parsePerson() throws XMLStreamException { + + final Person person = new Person(); + + while (reader.hasNext()) { + final int type = reader.next(); + + switch (type) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_NAME.equalsIgnoreCase(localName)){ + person.setName(reader.getElementText()); + }else if(TAG_AUTHOR_EMAIL.equalsIgnoreCase(localName)){ + person.setEmail(reader.getElementText()); + }else if(TAG_LINK.equalsIgnoreCase(localName)){ + person.setLink(parseLink()); + } + break; + case END_ELEMENT: + if(TAG_AUTHOR.equalsIgnoreCase(reader.getLocalName())){ + //end of the author element + return person; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, person tag without end."); + } + + /** + * Parse current Envelope element. + * The stax reader must be placed to the start element. + * + * @return Envelope + */ + private Envelope parseBound() throws XMLStreamException { + final String xmin = reader.getAttributeValue(null, ATT_BOUNDS_MINLON); + final String xmax = reader.getAttributeValue(null, ATT_BOUNDS_MAXLON); + final String ymin = reader.getAttributeValue(null, ATT_BOUNDS_MINLAT); + final String ymax = reader.getAttributeValue(null, ATT_BOUNDS_MAXLAT); + + if(xmin == null || xmax == null || ymin == null || ymax == null){ + throw new XMLStreamException("Error in xml file, metadata bounds not defined correctly"); + } + + toTagEnd(TAG_BOUNDS); + + return new ImmutableEnvelope(new double[] {Double.parseDouble(xmin), Double.parseDouble(ymin)}, + new double[] {Double.parseDouble(xmax), Double.parseDouble(ymax)}, + CommonCRS.WGS84.normalizedGeographic()); + } + + /** + * Parse way point type feature element. + * The stax reader must be placed to the start element. + * + * @return Feature + */ + private Feature parseWayPoint(final int index) throws XMLStreamException { + + final Feature feature = TYPE_WAYPOINT.newInstance(); + feature.setPropertyValue("index", index); + + //way points might be located in different tag names : wpt, rtept and trkpt + //we kind the current tag name to know when we reach the end. + final String tagName = reader.getLocalName(); + + List<URI> links = null; + + final String lat = reader.getAttributeValue(null, ATT_WPT_LAT); + final String lon = reader.getAttributeValue(null, ATT_WPT_LON); + + if(lat == null || lon == null){ + throw new XMLStreamException("Error in xml file, way point lat/lon not defined correctly"); + }else{ + feature.setPropertyValue("geometry", new Point(Double.parseDouble(lon), Double.parseDouble(lat))); + } + + while (reader.hasNext()) { + final int eventType = reader.next(); + + switch (eventType) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_WPT_ELE.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_ELE, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_TIME.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_TIME, parseTime(reader.getElementText())); + }else if(TAG_WPT_MAGVAR.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_MAGVAR, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_GEOIHEIGHT.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_GEOIHEIGHT, Double.valueOf(reader.getElementText())); + }else if(TAG_NAME.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_NAME, reader.getElementText()); + }else if(TAG_CMT.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_CMT,reader.getElementText()); + }else if(TAG_DESC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_DESC, reader.getElementText()); + }else if(TAG_SRC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_SRC, reader.getElementText()); + }else if(TAG_LINK.equalsIgnoreCase(localName)){ + if(links == null) links = new ArrayList<>(); + links.add(parseLink()); + }else if(TAG_WPT_SYM.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_SYM, reader.getElementText()); + }else if(TAG_TYPE.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_TYPE, reader.getElementText()); + }else if(TAG_WPT_FIX.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_FIX, reader.getElementText()); + }else if(TAG_WPT_SAT.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_SAT, Integer.valueOf(reader.getElementText())); + }else if(TAG_WPT_HDOP.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_HDOP, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_PDOP.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_PDOP, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_VDOP.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_VDOP, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_AGEOFGPSDATA.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_AGEOFGPSDATA, Double.valueOf(reader.getElementText())); + }else if(TAG_WPT_DGPSID.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_WPT_DGPSID, Integer.valueOf(reader.getElementText())); + }else if(version == GPXVersion.v1_0_0 && TAG_URL.equalsIgnoreCase(localName)){ + //GPX 1.0 only + if(links == null) links = new ArrayList<>(); + try { + links.add(new URI(reader.getElementText())); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + } + break; + case END_ELEMENT: + if(tagName.equalsIgnoreCase(reader.getLocalName())){ + //end of the way point element + if(links!=null) feature.setPropertyValue(TAG_LINK, links); + return feature; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, "+tagName+" tag without end."); + } + + /** + * Parse route type feature element. + * The stax reader must be placed to the start element. + * + * @return Feature + */ + private Feature parseRoute(final int index) throws XMLStreamException { + + final Feature feature = TYPE_ROUTE.newInstance(); + feature.setPropertyValue("index", index); + + int ptInc = 0; + List<URI> links = null; + List<Feature> wayPoints = null; + + while (reader.hasNext()) { + final int eventType = reader.next(); + + switch (eventType) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_RTE_RTEPT.equalsIgnoreCase(localName)){ + if(wayPoints == null) wayPoints = new ArrayList<>(); + wayPoints.add(parseWayPoint(ptInc++)); + }else if(TAG_NAME.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_NAME, reader.getElementText()); + }else if(TAG_CMT.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_CMT, reader.getElementText()); + }else if(TAG_DESC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_DESC, reader.getElementText()); + }else if(TAG_SRC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_SRC, reader.getElementText()); + }else if(TAG_LINK.equalsIgnoreCase(localName)){ + if(links == null) links = new ArrayList<>(); + links.add(parseLink()); + }else if(TAG_NUMBER.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_NUMBER, Integer.valueOf(reader.getElementText())); + }else if(TAG_TYPE.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_TYPE, reader.getElementText()); + }else if(version == GPXVersion.v1_0_0 && TAG_URL.equalsIgnoreCase(localName)){ + //GPX 1.0 only + if(links == null) links = new ArrayList<>(); + try { + links.add(new URI(reader.getElementText())); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + } + break; + case END_ELEMENT: + if(TAG_RTE.equalsIgnoreCase(reader.getLocalName())){ + //end of the route element + if(links!=null) feature.setPropertyValue(TAG_LINK, links); + if(wayPoints!=null) feature.setPropertyValue(TAG_RTE_RTEPT, wayPoints); + return feature; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, "+TAG_RTE+" tag without end."); + } + + /** + * Parse track segment type feature element. + * The stax reader must be placed to the start element. + * + * @return Feature + */ + private Feature parseTrackSegment(final int index) throws XMLStreamException { + + final Feature feature = TYPE_TRACK_SEGMENT.newInstance(); + feature.setPropertyValue("index", index); + int ptInc = 0; + List<Feature> wayPoints = null; + + while (reader.hasNext()) { + final int eventType = reader.next(); + + switch (eventType) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_TRK_SEG_PT.equalsIgnoreCase(localName)){ + if(wayPoints == null) wayPoints = new ArrayList<>(); + wayPoints.add(parseWayPoint(ptInc++)); + } + break; + case END_ELEMENT: + if(TAG_TRK_SEG.equalsIgnoreCase(reader.getLocalName())){ + //end of the track segment element + if(wayPoints!=null) feature.setPropertyValue(TAG_TRK_SEG_PT, wayPoints); + return feature; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, "+TAG_TRK_SEG+" tag without end."); + } + + /** + * Parse track type feature element. + * The stax reader must be placed to the start element. + * + * @return Feature + */ + private Feature parseTrack(final int index) throws XMLStreamException { + + final Feature feature = TYPE_TRACK.newInstance(); + feature.setPropertyValue("index", index); + int segInc = 0; + List<URI> links = null; + List<Feature> segments = null; + + while (reader.hasNext()) { + final int eventType = reader.next(); + + switch (eventType) { + case START_ELEMENT: + final String localName = reader.getLocalName(); + if(TAG_TRK_SEG.equalsIgnoreCase(localName)){ + if(segments == null) segments = new ArrayList<>(); + segments.add(parseTrackSegment(segInc++)); + }else if(TAG_NAME.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_NAME, reader.getElementText()); + }else if(TAG_CMT.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_CMT, reader.getElementText()); + }else if(TAG_DESC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_DESC, reader.getElementText()); + }else if(TAG_SRC.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_SRC, reader.getElementText()); + }else if(TAG_LINK.equalsIgnoreCase(localName)){ + if(links == null) links = new ArrayList<>(); + links.add(parseLink()); + }else if(TAG_NUMBER.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_NUMBER, Integer.valueOf(reader.getElementText())); + }else if(TAG_TYPE.equalsIgnoreCase(localName)){ + feature.setPropertyValue(TAG_TYPE, reader.getElementText()); + }else if(version == GPXVersion.v1_0_0 && TAG_URL.equalsIgnoreCase(localName)){ + //GPX 1.0 only + if(links == null) links = new ArrayList<>(); + try { + links.add(new URI(reader.getElementText())); + } catch (URISyntaxException ex) { + throw new XMLStreamException(ex); + } + } + break; + case END_ELEMENT: + if(TAG_TRK.equalsIgnoreCase(reader.getLocalName())){ + //end of the track element + if(links!=null) feature.setPropertyValue(TAG_LINK, links); + if(segments!=null) feature.setPropertyValue(TAG_TRK_SEG, segments); + return feature; + } + break; + } + } + + throw new XMLStreamException("Error in xml file, "+TAG_TRK+" tag without end."); + } + + /** + * Parse date or date time from string. + * The method support only ISO 8601 Date and DateTime formats. + * + * @param dateStr date in ISO date or data time format + * @return Temporal + */ + private static Temporal parseTime(String dateStr) { + try{ + final DateTimeFormatter format = DateTimeFormatter.ISO_DATE; + final TemporalAccessor accessor = format.parse(dateStr); + return LocalDate.from(accessor); + }catch(UnsupportedTemporalTypeException ex){ + final DateTimeFormatter format = DateTimeFormatter.ISO_DATE_TIME; + final TemporalAccessor accessor = format.parse(dateStr); + return LocalDateTime.from(accessor); + } + } + +}
Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXVersion.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXVersion.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXVersion.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXVersion.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import org.apache.sis.util.ArgumentChecks; + +/** + * GPX versions enumeration + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public enum GPXVersion { + /** + * GPX 1.0 + */ + v1_0_0, + /** + * GPX 1.1 + */ + v1_1_0; + + /** + * Convert code to GPXVersion enum. + * + * @param code gpx version as string + * @return enumeration + * @throws NumberFormatException if version is not formatted as expected + */ + public static GPXVersion toVersion(String code) throws NumberFormatException { + ArgumentChecks.ensureNonNull("code", code); + switch (code.trim()) { + case "1.0": + return v1_0_0; + case "1.1": + return v1_1_0; + default: + throw new NumberFormatException("Invalid version number " + code); + } + } +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPointsAsPolylineOperation.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPointsAsPolylineOperation.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPointsAsPolylineOperation.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPointsAsPolylineOperation.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import com.esri.core.geometry.Point; +import com.esri.core.geometry.Polyline; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.apache.sis.feature.AbstractAttribute; +import org.apache.sis.feature.AbstractOperation; +import org.apache.sis.feature.DefaultAttributeType; +import org.apache.sis.parameter.DefaultParameterDescriptorGroup; +import org.opengis.feature.AttributeType; +import org.opengis.feature.Feature; +import org.opengis.feature.IdentifiedType; +import org.opengis.feature.MultiValuedPropertyException; +import org.opengis.feature.Property; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.util.GenericName; + +/** + * A calculated attribute that define a Polyline geometry calculated + * from other attributes of the feature. + * + * For example : a boat that record it's position every hour. + * each record is available in a 0-N associate attribute. + * This class while extract each position and create a line as a new attribute. + * Any change applied to the positions will be visible on the line. + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public final class GroupPointsAsPolylineOperation extends AbstractOperation { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -5169104838093353092L; + + private static final AttributeType<Polyline> TYPE = new DefaultAttributeType<>( + Collections.singletonMap(NAME_KEY, "Polyline"),Polyline.class,1,1,null); + + /** + * An empty parameter value group for this operation. + */ + private static final ParameterDescriptorGroup EMPTY_PARAMS = + new DefaultParameterDescriptorGroup(Collections.singletonMap("name", "noargs"), 0, 1); + + private final GenericName[] path; + + /** + * + * @param name operation name + * @param attributePath names of the properties to group + */ + public GroupPointsAsPolylineOperation(GenericName name, GenericName ... attributePath) { + this(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name),attributePath); + } + + /** + * + * @param identification operation identification parameters + * @param attributePath names of the properties to group + */ + public GroupPointsAsPolylineOperation(Map<String, ?> identification, GenericName ... attributePath) { + super(identification); + this.path = attributePath; + } + + /** + * Returns an empty parameter descriptor group. + * @return Empty parameter descriptor + */ + @Override + public ParameterDescriptorGroup getParameters() { + return EMPTY_PARAMS; + } + + /** + * {@inheritDoc } + */ + @Override + public IdentifiedType getResult() { + return TYPE; + } + + /** + * {@inheritDoc } + */ + @Override + public Property apply(Feature feature, ParameterValueGroup parameters) { + return new GeomAtt(feature); + } + + private boolean explore(final Feature att, final int depth, Polyline geom, boolean first) { + if(depth == path.length-1){ + //we are on the field that hold the geometry points + for (final Object propVal : asCollection(att, path[depth])) { + if(first){ + geom.startPath(((Point)propVal)); + first = false; + }else{ + geom.lineTo(((Point)propVal)); + } + } + }else{ + //explore children + int d = depth+1; + for (final Object prop : asCollection(att,path[depth])) { + final Feature child = (Feature) prop; + first = explore(child, d, geom,first); + } + } + return first; + } + + private static Collection asCollection(Feature att, GenericName property) { + final Object value = att.getPropertyValue(property.toString()); + if(value == null) return Collections.EMPTY_LIST; + if(value instanceof Collection) return (Collection) value; + return Collections.singletonList(value); + } + + + /** + * Operation attribute. + * Value is calculated each time it is accessed. + */ + private final class GeomAtt extends AbstractAttribute<Polyline> { + + private static final long serialVersionUID = -8872834506769732436L; + + private final Feature feature; + + public GeomAtt(final Feature feature) { + super(TYPE); + this.feature = feature; + } + + @Override + public Polyline getValue() throws MultiValuedPropertyException { + final Polyline geom = new Polyline(); + explore(feature,0,geom,true); + return geom; + } + + @Override + public void setValue(Polyline value) { + throw new UnsupportedOperationException("Operation attribute can not be set."); + } + + } + +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPolylinesOperation.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPolylinesOperation.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPolylinesOperation.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GroupPolylinesOperation.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import com.esri.core.geometry.Polyline; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.apache.sis.feature.AbstractAttribute; +import org.apache.sis.feature.AbstractOperation; +import org.apache.sis.feature.DefaultAttributeType; +import org.apache.sis.parameter.DefaultParameterDescriptorGroup; +import org.opengis.feature.AttributeType; +import org.opengis.feature.Feature; +import org.opengis.feature.IdentifiedType; +import org.opengis.feature.MultiValuedPropertyException; +import org.opengis.feature.Property; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.parameter.ParameterValueGroup; +import org.opengis.util.GenericName; + +/** + * A calculated attribute that define a MultiLineString geometry calculated + * from other attributes of the feature. + * + * For example : a boat that record tracks every hour. + * each record is available in a 0-N complex attribute. + * This class while extract each track and create a Polyline as a new attribute. + * Any change applied to the tracks will be visible on the Polyline. + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public final class GroupPolylinesOperation extends AbstractOperation { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 7898989085371304159L; + + private static final AttributeType<Polyline> TYPE = new DefaultAttributeType<>( + Collections.singletonMap(NAME_KEY, "MultiLineString"),Polyline.class,1,1,null); + + /** + * An empty parameter value group for this operation. + */ + private static final ParameterDescriptorGroup EMPTY_PARAMS = + new DefaultParameterDescriptorGroup(Collections.singletonMap("name", "noargs"), 0, 1); + + private final GenericName[] path; + + /** + * + * @param name operation name + * @param attributePath names of the properties to group + */ + public GroupPolylinesOperation(GenericName name, GenericName ... attributePath) { + this(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name),attributePath); + } + + /** + * + * @param identification operation identification parameters + * @param attributePath names of the properties to group + */ + public GroupPolylinesOperation(Map<String, ?> identification, GenericName ... attributePath) { + super(identification); + this.path = attributePath; + } + + /** + * Returns an empty parameter descriptor group. + */ + @Override + public ParameterDescriptorGroup getParameters() { + return EMPTY_PARAMS; + } + + /** + * {@inheritDoc } + */ + @Override + public IdentifiedType getResult() { + return TYPE; + } + + /** + * {@inheritDoc } + */ + @Override + public Property apply(Feature feature, ParameterValueGroup parameters) { + return new GeomAtt(feature); + } + + private void explore(final Feature att, final int depth, final Polyline geom) { + if(depth == path.length-1){ + //we are on the field that hold the geometry + for (final Object propVal : asCollection(att, path[depth])) { + geom.add((Polyline)propVal, false); + } + }else{ + //explore childs + int d = depth+1; + for (final Object prop : asCollection(att,path[depth])) { + final Feature child = (Feature) prop; + explore(child, d, geom); + } + } + } + + private static Collection asCollection(Feature att, GenericName property) { + final Object value = att.getPropertyValue(property.toString()); + if(value == null) return Collections.EMPTY_LIST; + if(value instanceof Collection) return (Collection) value; + return Collections.singletonList(value); + } + + /** + * Operation attribute. + * Value is calculated each time it is accessed. + */ + private final class GeomAtt extends AbstractAttribute<Polyline> { + + private static final long serialVersionUID = 2246647654324667404L; + + private final Feature feature; + + public GeomAtt(final Feature feature) { + super(TYPE); + this.feature = feature; + } + + @Override + public Polyline getValue() throws MultiValuedPropertyException { + final Polyline geom = new Polyline(); + explore(feature,0,geom); + return geom; + } + + @Override + public void setValue(Polyline value) { + throw new UnsupportedOperationException("Operation attribute can not be set."); + } + + } + +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/MetaData.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/MetaData.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/MetaData.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/MetaData.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.List; +import org.apache.sis.io.TableAppender; + +import org.opengis.geometry.Envelope; + +/** + * Metadata object as defined in GPX. + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public class MetaData { + + private String name; + private String description; + private Person person; + private CopyRight copyRight; + private final List<URI> links = new ArrayList<>(); + private Temporal time; + private String keywords; + private Envelope bounds; + + /** + * Returns the dataset name. + * + * @return name, may be null + */ + public String getName() { + return name; + } + + /** + * Set dataset name. + * + * @param name, can be null + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the dataset description. + * + * @return description, may be null + */ + public String getDescription() { + return description; + } + + /** + * Set dataset description. + * + * @param description, can be null + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Returns dataset author person. + * + * @return Person, may be null + */ + public Person getPerson() { + return person; + } + + /** + * Set dataset author person. + * + * @param person, can be null + */ + public void setPerson(Person person) { + this.person = person; + } + + /** + * Returns the dataset copyright + * + * @return copyright, may be null + */ + public CopyRight getCopyRight() { + return copyRight; + } + + /** + * Set dataset copyright. + * + * @param copyRight, can be null + */ + public void setCopyRight(CopyRight copyRight) { + this.copyRight = copyRight; + } + + /** + * Returns the dataset links + * The returned list is modifiable. + * + * @return list of links, never null + */ + public List<URI> getLinks() { + return links; + } + + /** + * Returns the dataset creation time. + * + * @return Temporal, may be null + */ + public Temporal getTime() { + return time; + } + + /** + * Set dataset creation time. + * + * @param time, can be null + */ + public void setTime(Temporal time) { + this.time = time; + } + + /** + * Returns dataset keywords. + * + * @return keywords, may be null + */ + public String getKeywords() { + return keywords; + } + + /** + * Set dataset keywords. + * + * @param keywords, can be null + */ + public void setKeywords(String keywords) { + this.keywords = keywords; + } + + /** + * Returns the dataset bounding box. + * + * @return Envelope, may be null + */ + public Envelope getBounds() { + return bounds; + } + + /** + * Set dataset bounding box. + * + * @param bounds, can be null + */ + public void setBounds(Envelope bounds) { + this.bounds = bounds; + } + + @Override + public String toString() { + final StringWriter writer = new StringWriter(); + final TableAppender tablewriter = new TableAppender(writer); + + tablewriter.appendHorizontalSeparator(); + tablewriter.append("GPX-Metadata\t \n"); + tablewriter.appendHorizontalSeparator(); + + tablewriter.append("Name\t"+getName()+"\n"); + tablewriter.append("Desc\t"+getDescription()+"\n"); + tablewriter.append("Time\t"+getTime()+"\n"); + tablewriter.append("Keywords\t"+getKeywords()+"\n"); + tablewriter.append("Bounds\t"+getBounds()+"\n"); + + final Person person = getPerson(); + if(person != null){ + tablewriter.append("Person - Name\t"+person.getName()+"\n"); + tablewriter.append("Person - EMail\t"+person.getEmail()+"\n"); + tablewriter.append("Person - Link\t"+person.getLink()+"\n"); + }else{ + tablewriter.append("Person\t"+person+"\n"); + } + + final CopyRight copyright = getCopyRight(); + if(copyright != null){ + tablewriter.append("CopyRight - Author\t"+copyright.getAuthor()+"\n"); + tablewriter.append("CopyRight - Year\t"+copyright.getYear()+"\n"); + tablewriter.append("CopyRight - License\t"+copyright.getLicense()+"\n"); + }else{ + tablewriter.append("CopyRight\t"+copyright+"\n"); + } + + tablewriter.append("Links\t"); + final List<URI> links = getLinks(); + if(links.isEmpty()){ + tablewriter.append("None\n"); + }else{ + tablewriter.append("\n"); + for(final URI uri : getLinks()){ + tablewriter.append("\t"+uri+"\n"); + } + } + + tablewriter.appendHorizontalSeparator(); + + try { + tablewriter.flush(); + writer.flush(); + } catch (IOException ex) { + //will never happen is this case + ex.printStackTrace(); + } + + return writer.getBuffer().toString(); + } + +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/Person.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/Person.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/Person.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/Person.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.gpx; + +import java.net.URI; + +/** + * Person object as defined in GPX. + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public class Person { + + private String name; + private String email; + private URI link; + + /** + * Returns the person name. + * + * @return name, may be null + */ + public String getName() { + return name; + } + + /** + * Set person name. + * + * @param name, can be null + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the person email. + * + * @return email, may be null + */ + public String getEmail() { + return email; + } + + /** + * Set person email. + * + * @param email, can be null + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Returns the person information link. + * + * @return uri, may be null + */ + public URI getLink() { + return link; + } + + /** + * Set person information link. + * + * @param link, can be null + */ + public void setLink(URI link) { + this.link = link; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Person("); + sb.append(name).append(',').append(email).append(',').append(link); + sb.append(')'); + return sb.toString(); + } + +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/AbstractConfigurable.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/AbstractConfigurable.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/AbstractConfigurable.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/AbstractConfigurable.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.xml; + +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Guilhem Legal (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public abstract class AbstractConfigurable { + + /** + * Map of optional properties to configure the underlying object. + * + */ + protected final Map<String, Object> properties = new HashMap<>(); + + /** + * + * @param key property key + * @return value, may be null + */ + public Object getProperty(final String key) { + return properties.get(key); + } + + /** + * @return the properties + */ + public Map<String, Object> getProperties() { + return properties; + } + +} Added: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/StaxStreamReader.java URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/StaxStreamReader.java?rev=1738241&view=auto ============================================================================== --- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/StaxStreamReader.java (added) +++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/xml/StaxStreamReader.java Fri Apr 8 12:43:53 2016 @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sis.internal.xml; + +import java.io.*; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.Result; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.dom.DOMSource; +import org.w3c.dom.Node; + +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; + +import static javax.xml.stream.XMLStreamReader.*; +import javax.xml.stream.util.StreamReaderDelegate; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.stax.StAXSource; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * An abstract class for all stax parser.<br> + * Readers for a given specification should extend this class and + * provide appropriate read methods.<br> + * <br> + * Example : <br> + * <pre> + * {@code + * public class UserReader extends StaxStreamReader{ + * + * public User read() throws XMLStreamException{ + * //casual stax reading operations + * return user; + * } + * } + * } + * </pre> + * And should be used like :<br> + * <pre> + * {@code + * final UserReader instance = new UserReader(); + * try{ + * instance.setInput(stream); + * user = instance.read(); + * }finally{ + * instance.dispose(); + * } + * } + * </pre> + * + * @author Johann Sorel (Geomatys) + * @since 0.7 + * @version 0.7 + * @module + */ +public abstract class StaxStreamReader extends AbstractConfigurable implements AutoCloseable { + + /** + * Wrapped xml stream reader + */ + protected XMLStreamReader reader; + + /** + * Store the input stream if it was generated by the parser itself. + * It will closed on the dispose method or when a new input is set. + */ + private InputStream sourceStream; + + /** + * close potentiel previous stream and cache if there are some. + * This way the reader can be reused for a different input later. + * The underlying stax reader will be closed. + * + * @throws java.io.IOException if previous source stream caused an exception on close + * @throws javax.xml.stream.XMLStreamException if previous stax reader caused an exception on close + */ + public void reset() throws IOException, XMLStreamException { + if(sourceStream != null){ + sourceStream.close(); + sourceStream = null; + } + if(reader != null){ + reader.close(); + reader = null; + } + } + + /** + * Release potentiel locks or opened stream. + * Must be called when the reader is not needed anymore. + * It should not be used after this method has been called. + */ + @Override + public void close() throws Exception { + reset(); + } + + /** + * Set the input for this reader.<br> + * Handle types are :<br> + * - java.io.File<br> + * - java.io.Reader<br> + * - java.io.InputStream<br> + * - java.net.URL<br> + * - java.net.URI<br> + * - javax.xml.stream.XMLStreamReader<br> + * - javax.xml.transform.Source<br> + * + * @param input input object + * @throws IOException if input failed to be opened for any IO reason + * @throws XMLStreamException if input is not a valid XML stream + */ + public void setInput(Object input) throws IOException, XMLStreamException { + reset(); + + if(input instanceof XMLStreamReader){ + reader = (XMLStreamReader) input; + return; + } + + if(input instanceof File){ + sourceStream = new FileInputStream((File)input); + input = sourceStream; + }else if(input instanceof Path){ + sourceStream = Files.newInputStream((Path)input, StandardOpenOption.READ); + input = sourceStream; + }else if(input instanceof URL){ + sourceStream = ((URL)input).openStream(); + input = sourceStream; + }else if(input instanceof URI){ + sourceStream = ((URI)input).toURL().openStream(); + input = sourceStream; + }else if(input instanceof String){ + input = new StringReader((String) input); + } + + reader = toReader(input); + } + + /** + * Iterator on the reader until it reachs the end of the given tag name. + * @param tagName tag name to search + * @throws XMLStreamException if end tag could not be found + * or if there is error processing the xml stream + */ + protected void toTagEnd(final String tagName) throws XMLStreamException { + while (reader.hasNext()) { + if(END_ELEMENT == reader.next() && + tagName.equalsIgnoreCase(reader.getLocalName())) + return; + } + throw new XMLStreamException("Error in xml file, Could not find end of tag "+tagName+" ."); + } + + /** + * Creates a new XMLStreamReader. + * @param input input to convert to stax stream + * @return XMLStreamReader + * @throws XMLStreamException if the input is not handled + */ + private static final XMLStreamReader toReader(final Object input) + throws XMLStreamException { + final XMLInputFactory XMLfactory = XMLInputFactory.newInstance(); + XMLfactory.setProperty("http://java.sun.com/xml/stream/properties/report-cdata-event", Boolean.TRUE); + XMLfactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE); + + if (input instanceof InputStream) { + return XMLfactory.createXMLStreamReader((InputStream) input); + } else if (input instanceof Source) { + return XMLfactory.createXMLStreamReader((Source) input); + } else if (input instanceof Node) { + + /* here we can think that we can use a DOMSource and pass it directly to the + * method XMLfactory.createXMLStreamReader(Source) but it lead to a NPE + * during the geometry unmarshall. + */ + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Result outputTarget = new StreamResult(outputStream); + Transformer t = TransformerFactory.newInstance().newTransformer(); + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + t.transform(new DOMSource((Node) input), outputTarget); + return XMLfactory.createXMLStreamReader(new ByteArrayInputStream(outputStream.toByteArray())); + } catch (TransformerException ex) { + throw new XMLStreamException(ex); + } + } else if (input instanceof Reader) { + return XMLfactory.createXMLStreamReader((Reader) input); + } else { + throw new XMLStreamException("Input type is not supported : " + input); + } + } + + /** + * <p>XML language provides two notations for boolean type : + * "true" can be written "1" and "0" significates "false". + * This method considers all this values as CharSequences and return its boolean value.</p> + * + * @param candidate The String to parse + * @return true if bool is equal to "true" or "1". + */ + protected static boolean parseBoolean(final String candidate) { + if (candidate.length() == 1) { + return !candidate.equals("0"); + } + return Boolean.parseBoolean(candidate); + } + + /** + * <p>This method reads doubles with coma separated.</p> + * + * @param candidate Can not be null. + * @return parsed double value + */ + protected static double parseDouble(final String candidate) { + return Double.parseDouble(candidate.replace(',', '.')); + } + + /** + * Iterator on the reader until it reach the end of the given tag name. + * Return the read elements as dom. + * + * @param tagName tag + * @return Document read elements as a dom document + * @throws javax.xml.stream.XMLStreamException if conversion to dom failed + */ + protected Document readAsDom(final String tagName) throws XMLStreamException { + + final XMLStreamReader limitedReader = new StreamReaderDelegate(reader){ + boolean finished = false; + + @Override + public boolean hasNext() throws XMLStreamException { + if(finished) return false; + return super.hasNext(); + } + + @Override + public int next() throws XMLStreamException { + int t = super.next(); + finished = END_ELEMENT == t && tagName.equalsIgnoreCase(reader.getLocalName()); + return t; + } + + }; + + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + + final TransformerFactory trsFactory = TransformerFactory.newInstance(); + try { + final DocumentBuilder builder = factory.newDocumentBuilder(); + final Transformer idTransform = trsFactory.newTransformer(); + final Source input = new StAXSource(limitedReader); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final Result output = new StreamResult(out); + idTransform.transform(input, output); + final Document doc = builder.parse(new ByteArrayInputStream(out.toByteArray())); + return doc; + } catch (TransformerConfigurationException e) { + throw new XMLStreamException(e.getMessage()); + } catch (TransformerFactoryConfigurationError e) { + throw new XMLStreamException(e.getMessage()); + } catch (IOException e) { + throw new XMLStreamException(e.getMessage()); + } catch (TransformerException e) { + throw new XMLStreamException(e.getMessage()); + } catch (SAXException e) { + throw new XMLStreamException(e.getMessage()); + } catch (ParserConfigurationException e) { + throw new XMLStreamException(e.getMessage()); + } + + } + +}
