/* Copyright (c) 2013-2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Victor Olaya (Boundless) - initial implementation */ package org.locationtech.geogig.storage.text; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Serializable; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import org.geotools.feature.NameImpl; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.locationtech.geogig.api.Bucket; import org.locationtech.geogig.api.CommitBuilder; import org.locationtech.geogig.api.Node; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.RevCommit; import org.locationtech.geogig.api.RevFeature; import org.locationtech.geogig.api.RevFeatureImpl; import org.locationtech.geogig.api.RevFeatureType; import org.locationtech.geogig.api.RevFeatureTypeImpl; import org.locationtech.geogig.api.RevObject; import org.locationtech.geogig.api.RevObject.TYPE; import org.locationtech.geogig.api.RevPerson; import org.locationtech.geogig.api.RevPersonImpl; import org.locationtech.geogig.api.RevTag; import org.locationtech.geogig.api.RevTagImpl; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.RevTreeImpl; import org.locationtech.geogig.storage.FieldType; import org.locationtech.geogig.storage.ObjectReader; import org.locationtech.geogig.storage.ObjectSerializingFactory; import org.locationtech.geogig.storage.ObjectWriter; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.AttributeType; import org.opengis.feature.type.FeatureTypeFactory; import org.opengis.feature.type.GeometryType; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.feature.type.PropertyType; import org.opengis.filter.Filter; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.InternationalString; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; /** * An {@link ObjectSerialisingFactory} for the {@link RevObject}s text format. * <p> * The following formats are used to interchange {@link RevObject} instances in a text format: * <H3>Commit:</H3> * * <pre> * {@code COMMIT\n} * {@code "tree" + "\t" + <tree id> + "\n"} * {@code "parents" + "\t" + <parent id> [+ " " + <parent id>...] + "\n"} * {@code "author" + "\t" + <author name> + " " + <author email> + "\t" + <author_timestamp> + "\t" + <author_timezone_offset> + "\n"} * {@code "committer" + "\t" + <committer name> + " " + <committer email> + "\t" + <committer_timestamp> + "\t" + <committer_timezone_offset> + "\n"} * * {@code "message" + "\t" + <message> + "\n"} * </pre> * * <H3>Tree:</H3> * * <pre> * {@code TREE\n} * {@code "size" + "\t" + <size> + "\n"} * {@code "numtrees" + "\t" + <numtrees> + "\n"} * * {@code "BUCKET" + "\t" + <bucket_idx> + "\t" + <ObjectId> + "\t" + <bounds> + "\n"} * or * {@code "REF" + "\t" + <ref_type> + "\t" + <ref_name> + "\t" + <ObjectId> + "\t" + <MetadataId> + "\t" + <bounds>"\n"} * . * . * . * </pre> * * <H3>Feature:</H3> * * <pre> * {@code FEATURE\n} * {@code "<attribute_class_1>" + "\t" + <attribute_value_1> + "\n"} * . * . * . * {@code "<attribute_class_n>" + "\t" + <attribute_value_n> + "\n"} * For array types, values are written as a space-separated list of single values, enclosed between square brackets * </pre> * * <H3>FeatureType:</H3> * * <pre> * {@code FEATURE_TYPE\n} * {@code "name" + "\t" + <name> + "\n"} * {@code "<attribute_name>" + "\t" + <attribute_type> + "\t" + <min_occur> + "\t" + <max_occur> + "\t" + <nillable <True|False>> + "\n"} * {@code "<attribute_name>" + "\t" + <attribute_type> + "\t" + <min_occur> + "\t" + <max_occur> + "\t" + <nillable <True|False>> + "\n"} * . * . * . * * </pre> * * <H3>Tag:</H3> * * <pre> * {@code "id" + "\t" + <id> + "\n"} * ... * </pre> */ public class TextSerializationFactory implements ObjectSerializingFactory { // protected static final String NULL = "null"; @Override public ObjectReader<RevCommit> createCommitReader() { return COMMIT_READER; } @Override public ObjectReader<RevTree> createRevTreeReader() { return TREE_READER; } @Override public ObjectReader<RevFeature> createFeatureReader() { return FEATURE_READER; } @Override public ObjectReader<RevFeature> createFeatureReader(Map<String, Serializable> hints) { // TODO return FEATURE_READER; } @Override public ObjectReader<RevFeatureType> createFeatureTypeReader() { return FEATURETYPE_READER; } @SuppressWarnings("unchecked") @Override public <T extends RevObject> ObjectWriter<T> createObjectWriter(TYPE type) { switch (type) { case COMMIT: return (ObjectWriter<T>) COMMIT_WRITER; case FEATURE: return (ObjectWriter<T>) FEATURE_WRITER; case FEATURETYPE: return (ObjectWriter<T>) FEATURETYPE_WRITER; case TREE: return (ObjectWriter<T>) TREE_WRITER; case TAG: return (ObjectWriter<T>) TAG_WRITER; default: throw new IllegalArgumentException("Unknown or unsupported object type: " + type); } } @SuppressWarnings("unchecked") @Override public <T extends RevObject> ObjectReader<T> createObjectReader(TYPE type) { switch (type) { case COMMIT: return (ObjectReader<T>) COMMIT_READER; case FEATURE: return (ObjectReader<T>) FEATURE_READER; case FEATURETYPE: return (ObjectReader<T>) FEATURETYPE_READER; case TREE: return (ObjectReader<T>) TREE_READER; case TAG: return (ObjectReader<T>) TAG_READER; default: throw new IllegalArgumentException("Unknown or unsupported object type: " + type); } } @Override public ObjectReader<RevObject> createObjectReader() { return OBJECT_READER; } /** * Abstract text writer that provides print methods on a {@link Writer} to consistently write * newlines as {@code \n} instead of using the platform's line separator as in * {@link PrintWriter}. It also provides some common method used by different writers. */ private static abstract class TextWriter<T extends RevObject> implements ObjectWriter<T> { public static final String NULL_BOUNDING_BOX = "null"; /** * Different types of tree nodes. */ public enum TreeNode { REF, BUCKET; } @Override public void write(T object, OutputStream out) throws IOException { OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8"); println(writer, object.getType().name()); print(object, writer); writer.flush(); } protected abstract void print(T object, Writer w) throws IOException; protected void print(Writer w, CharSequence... s) throws IOException { if (s == null) { return; } for (CharSequence c : s) { w.write(String.valueOf(c)); } } protected void println(Writer w, CharSequence... s) throws IOException { if (s != null) { print(w, s); } w.write('\n'); } protected void writeNode(Writer w, Node node) throws IOException { print(w, TreeNode.REF.name()); print(w, "\t"); print(w, node.getType().name()); print(w, "\t"); print(w, node.getName()); print(w, "\t"); print(w, node.getObjectId().toString()); print(w, "\t"); print(w, node.getMetadataId().or(ObjectId.NULL).toString()); print(w, "\t"); Envelope envHelper = new Envelope(); writeBBox(w, node, envHelper); println(w); } protected void writeBBox(Writer w, Node node, Envelope envHelper) throws IOException { envHelper.setToNull(); node.expand(envHelper); writeEnvelope(w, envHelper); } protected void writeEnvelope(Writer w, Envelope envHelper) throws IOException { if (envHelper.isNull()) { print(w, TextWriter.NULL_BOUNDING_BOX); return; } print(w, Double.toString(envHelper.getMinX())); print(w, ";"); print(w, Double.toString(envHelper.getMaxX())); print(w, ";"); print(w, Double.toString(envHelper.getMinY())); print(w, ";"); print(w, Double.toString(envHelper.getMaxY())); } } /** * Commit writer. * <p> * Output format: * * <pre> * {@code COMMIT\n} * {@code "tree" + "\t" + <tree id> + "\n"} * {@code "parents" + "\t" + <parent id> [+ " " + <parent id>...] + "\n"} * {@code "author" + "\t" + <author name> + " " + <author email> + "\t" + <author_timestamp> + "\t" + <author_timezone_offset> + "\n"} * {@code "committer" + "\t" + <committer name> + " " + <committer email> + "\t" + <committer_timestamp> + "\t" + <committer_timezone_offset> + "\n"} * * {@code "message" + "\t" + <message> + "\n"} * </pre> * */ private static final TextWriter<RevCommit> COMMIT_WRITER = new TextWriter<RevCommit>() { @Override protected void print(RevCommit commit, Writer w) throws IOException { println(w, "tree\t", commit.getTreeId().toString()); print(w, "parents\t"); for (Iterator<ObjectId> it = commit.getParentIds().iterator(); it.hasNext();) { print(w, it.next().toString()); if (it.hasNext()) { print(w, " "); } } println(w); printPerson(w, "author", commit.getAuthor()); printPerson(w, "committer", commit.getCommitter()); println(w, "message\t", Optional.fromNullable(commit.getMessage()).or("")); w.flush(); } private void printPerson(Writer w, String name, RevPerson person) throws IOException { print(w, name); print(w, "\t"); print(w, person.getName().or(" ")); print(w, "\t"); print(w, person.getEmail().or(" ")); print(w, "\t"); print(w, Long.toString(person.getTimestamp())); print(w, "\t"); print(w, Long.toString(person.getTimeZoneOffset())); println(w); } }; /** * Feature writer. * <p> * Output format: * * <pre> * {@code FEATURE\n} * {@code "<attribute_class_1>" + "\t" + <attribute_value_1> + "\n"} * . * . * . * {@code "<attribute_class_n>" + "\t" + <attribute_value_n> + "\n"} * For array types, values are written as a space-separated list of single values, enclosed between square brackets * </pre> * */ private static final TextWriter<RevFeature> FEATURE_WRITER = new TextWriter<RevFeature>() { @Override protected void print(RevFeature feature, Writer w) throws IOException { ImmutableList<Optional<Object>> values = feature.getValues(); for (Optional<Object> opt : values) { final FieldType type = FieldType.forValue(opt); String valueString = TextValueSerializer.asString(opt); println(w, type.toString() + "\t" + valueString); } w.flush(); } }; /** * Feature type writer. * <p> * Output format: * * <pre> * {@code FEATURE_TYPE\n} * {@code "name" + "\t" + <name> + "\n"} * {@code "<attribute_name>" + "\t" + <attribute_type> + "\t" + <min_occur> + "\t" + <max_occur> + "\t" + <nillable <True|False>> + "\n"} * {@code "<attribute_name>" + "\t" + <attribute_type> + "\t" + <min_occur> + "\t" + <max_occur> + "\t" + <nillable <True|False>> + "\n"} * . * . * . * * </pre> * * Geometry attributes have an extra token per line representing the crs * */ private static final TextWriter<RevFeatureType> FEATURETYPE_WRITER = new TextWriter<RevFeatureType>() { @Override protected void print(RevFeatureType featureType, Writer w) throws IOException { println(w, "name\t", featureType.getName().toString()); Collection<PropertyDescriptor> attribs = featureType.type().getDescriptors(); for (PropertyDescriptor attrib : attribs) { printAttributeDescriptor(w, attrib); } w.flush(); } private void printAttributeDescriptor(Writer w, PropertyDescriptor attrib) throws IOException { print(w, attrib.getName().toString()); print(w, "\t"); print(w, FieldType.forBinding(attrib.getType().getBinding()).name()); print(w, "\t"); print(w, Integer.toString(attrib.getMinOccurs())); print(w, "\t"); print(w, Integer.toString(attrib.getMaxOccurs())); print(w, "\t"); print(w, Boolean.toString(attrib.isNillable())); PropertyType attrType = attrib.getType(); if (attrType instanceof GeometryType) { GeometryType gt = (GeometryType) attrType; CoordinateReferenceSystem crs = gt.getCoordinateReferenceSystem(); String crsText = CrsTextSerializer.serialize(crs); print(w, "\t"); println(w, crsText); } else { println(w, ""); } } }; /** * Tree writer. * <p> * Output format: * * <pre> * {@code TREE\n} * {@code "size" + "\t" + <size> + "\n"} * {@code "numtrees" + "\t" + <numtrees> + "\n"} * * {@code "BUCKET" + "\t" + <bucket_idx> + "\t" + <ObjectId> + "\t" + <bounds> + "\n"} * or * {@code "REF" + "\t" + <ref_type> + "\t" + <ref_name> + "\t" + <ObjectId> + "\t" + <MetadataId> + "\t" + <bounds>"\n"} * . * . * . * </pre> */ private static final TextWriter<RevTree> TREE_WRITER = new TextWriter<RevTree>() { @Override protected void print(RevTree revTree, Writer w) throws IOException { println(w, "size\t", Long.toString(revTree.size())); println(w, "numtrees\t", Integer.toString(revTree.numTrees())); if (revTree.trees().isPresent()) { writeChildren(w, revTree.trees().get()); } if (revTree.features().isPresent()) { writeChildren(w, revTree.features().get()); } else if (revTree.buckets().isPresent()) { writeBuckets(w, revTree.buckets().get()); } } private void writeChildren(Writer w, ImmutableCollection<Node> children) throws IOException { for (Node ref : children) { writeNode(w, ref); } } private void writeBuckets(Writer w, ImmutableSortedMap<Integer, Bucket> buckets) throws IOException { for (Entry<Integer, Bucket> entry : buckets.entrySet()) { Integer bucketIndex = entry.getKey(); Bucket bucket = entry.getValue(); print(w, TreeNode.BUCKET.name()); print(w, "\t"); print(w, bucketIndex.toString()); print(w, "\t"); print(w, bucket.id().toString()); print(w, "\t"); Envelope env = new Envelope(); env.setToNull(); bucket.expand(env); writeEnvelope(w, env); println(w); } } }; /** * Tag writer. * <p> * Output format: * * <pre> * {@code TAG\n} * {@code "name" + "\t" + <tagname> + "\n"} * {@code "commitid" + "\t" + <comitid> + "\n"} * {@code "message" + "\t" + <message> + "\n"} * {@code "tagger" + "\t" + <tagger name> + " " + <tagger email> + "\t" + <tagger> + "\t" + <tagger_timezone_offset> + "\n"} * </pre> * */ private static final TextWriter<RevTag> TAG_WRITER = new TextWriter<RevTag>() { @Override protected void print(RevTag tag, Writer w) throws IOException { println(w, "name\t", tag.getName()); println(w, "commitid\t", tag.getCommitId().toString()); println(w, "message\t", tag.getMessage()); print(w, "tagger"); print(w, "\t"); print(w, tag.getTagger().getName().or(" ")); print(w, "\t"); print(w, tag.getTagger().getEmail().or(" ")); print(w, "\t"); print(w, Long.toString(tag.getTagger().getTimestamp())); print(w, "\t"); print(w, Long.toString(tag.getTagger().getTimeZoneOffset())); println(w); w.flush(); } }; private abstract static class TextReader<T extends RevObject> implements ObjectReader<T> { @Override public T read(ObjectId id, InputStream rawData) throws IllegalArgumentException { try { BufferedReader reader; reader = new BufferedReader(new InputStreamReader(rawData, "UTF-8")); TYPE type = RevObject.TYPE.valueOf(requireLine(reader).trim()); T parsed = read(id, reader, type); Preconditions.checkState(parsed != null, "parsed to null"); if (id != null) { Preconditions .checkState(id.equals(parsed.getId()), "Expected and parsed object ids don't match: %s %s", id, parsed.getId()); } return parsed; } catch (Exception e) { throw Throwables.propagate(e); } } protected String parseLine(String line, String expectedHeader) throws IOException { List<String> fields = Lists.newArrayList(Splitter.on('\t').split(line)); Preconditions.checkArgument(fields.size() == 2, "Expected %s\\t<...>, got '%s'", expectedHeader, line); Preconditions.checkArgument(expectedHeader.equals(fields.get(0)), "Expected field %s, got '%s'", expectedHeader, fields.get(0)); String value = fields.get(1); return value; } protected abstract T read(ObjectId id, BufferedReader reader, TYPE type) throws IOException; protected Node parseNodeLine(String line) { List<String> tokens = Lists.newArrayList(Splitter.on('\t').split(line)); Preconditions.checkArgument(tokens.size() == 6, "Wrong tree element definition: %s", line); TYPE type = TYPE.valueOf(tokens.get(1)); String name = tokens.get(2); ObjectId id = ObjectId.valueOf(tokens.get(3)); ObjectId metadataId = ObjectId.valueOf(tokens.get(4)); Envelope bbox = parseBBox(tokens.get(5)); org.locationtech.geogig.api.Node ref = org.locationtech.geogig.api.Node.create(name, id, metadataId, type, bbox); return ref; } protected Envelope parseBBox(String s) { if (s.equals(TextWriter.NULL_BOUNDING_BOX)) { return new Envelope(); } List<String> tokens = Lists.newArrayList(Splitter.on(';').split(s)); Preconditions.checkArgument(tokens.size() == 4, "Wrong bounding box definition: %s", s); double minx = Double.parseDouble(tokens.get(0)); double maxx = Double.parseDouble(tokens.get(1)); double miny = Double.parseDouble(tokens.get(2)); double maxy = Double.parseDouble(tokens.get(3)); Envelope bbox = new Envelope(minx, maxx, miny, maxy); return bbox; } } private static final TextReader<RevObject> OBJECT_READER = new TextReader<RevObject>() { @Override protected RevObject read(ObjectId id, BufferedReader read, TYPE type) throws IOException { switch (type) { case COMMIT: return COMMIT_READER.read(id, read, type); case FEATURE: return FEATURE_READER.read(id, read, type); case TREE: return TREE_READER.read(id, read, type); case FEATURETYPE: return FEATURETYPE_READER.read(id, read, type); case TAG: return TAG_READER.read(id, read, type); default: throw new IllegalArgumentException("Unknown object type " + type); } } }; /** * Commit reader. * <p> * Parses a commit of the format: * * <pre> * {@code COMMIT\n} * {@code "tree" + "\t" + <tree id> + "\n"} * {@code "parents" + "\t" + <parent id> [+ " " + <parent id>...] + "\n"} * {@code "author" + "\t" + <author name> + " " + <author email> + "\t" + <author_timestamp> + "\t" + <author_timezone_offset> + "\n"} * {@code "committer" + "\t" + <committer name> + " " + <committer email> + "\t" + <committer_timestamp> + "\t" + <committer_timezone_offset> + "\n"} * * {@code "message" + "\t" + <message> + "\n"} * </pre> * */ private static final TextReader<RevCommit> COMMIT_READER = new TextReader<RevCommit>() { @Override protected RevCommit read(ObjectId id, BufferedReader reader, TYPE type) throws IOException { Preconditions.checkArgument(TYPE.COMMIT.equals(type), "Wrong type: %s", type.name()); String tree = parseLine(requireLine(reader), "tree"); List<String> parents = Lists.newArrayList(Splitter.on(' ').omitEmptyStrings() .split(parseLine(requireLine(reader), "parents"))); RevPerson author = parsePerson(requireLine(reader), "author"); RevPerson committer = parsePerson(requireLine(reader), "committer"); String message = parseMessage(reader); CommitBuilder builder = new CommitBuilder(); builder.setAuthor(author.getName().orNull()); builder.setAuthorEmail(author.getEmail().orNull()); builder.setAuthorTimestamp(author.getTimestamp()); builder.setAuthorTimeZoneOffset(author.getTimeZoneOffset()); builder.setCommitter(committer.getName().orNull()); builder.setCommitterEmail(committer.getEmail().orNull()); builder.setCommitterTimestamp(committer.getTimestamp()); builder.setCommitterTimeZoneOffset(committer.getTimeZoneOffset()); builder.setMessage(message); List<ObjectId> parentIds = Lists.newArrayList(Iterators.transform(parents.iterator(), new Function<String, ObjectId>() { @Override public ObjectId apply(String input) { ObjectId objectId = ObjectId.valueOf(input); return objectId; } })); builder.setParentIds(parentIds); builder.setTreeId(ObjectId.valueOf(tree)); RevCommit commit = builder.build(); return commit; } private RevPerson parsePerson(String line, String expectedHeader) throws IOException { String[] tokens = line.split("\t"); Preconditions.checkArgument(expectedHeader.equals(tokens[0]), "Expected field %s, got '%s'", expectedHeader, tokens[0]); String name = tokens[1].trim().isEmpty() ? null : tokens[1]; String email = tokens[2].trim().isEmpty() ? null : tokens[2]; long timestamp = Long.parseLong(tokens[3]); int offset = Integer.parseInt(tokens[4]); return new RevPersonImpl(name, email, timestamp, offset); } private String parseMessage(BufferedReader reader) throws IOException { StringBuilder msg = new StringBuilder(parseLine(requireLine(reader), "message")); String extraLine; while ((extraLine = reader.readLine()) != null) { msg.append('\n').append(extraLine); } return msg.toString(); } }; /** * Feature reader. * <p> * Parses a feature in the format: * * <pre> * {@code "<attribute class_1>" + "\t" + <attribute_value_1> + "\n"} * . * . * . * {@code "<attribute class_n>" + "\t" + <attribute_value_n> + "\n"} * * Array values are written as a space-separated list of single values, enclosed between square brackets * </pre> * */ private static final TextReader<RevFeature> FEATURE_READER = new TextReader<RevFeature>() { @Override protected RevFeature read(ObjectId id, BufferedReader reader, TYPE type) throws IOException { Preconditions.checkArgument(TYPE.FEATURE.equals(type), "Wrong type: %s", type.name()); List<Object> values = Lists.newArrayList(); String line; while ((line = reader.readLine()) != null) { values.add(parseAttribute(line)); } ImmutableList.Builder<Optional<Object>> valuesBuilder = new ImmutableList.Builder<Optional<Object>>(); for (Object value : values) { valuesBuilder.add(Optional.fromNullable(value)); } return RevFeatureImpl.build(valuesBuilder.build()); } private Object parseAttribute(String line) { List<String> tokens = Lists.newArrayList(Splitter.on('\t').split(line)); Preconditions.checkArgument(tokens.size() == 2, "Wrong attribute definition: %s", line); String typeName = tokens.get(0); String value = tokens.get(1); FieldType type; try { type = FieldType.valueOf(typeName); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Wrong type name: " + typeName); } return TextValueSerializer.fromString(type, value); } }; /** * Feature type reader. * <p> * Parses a feature type in the format: * * <pre> * {@code "id" + "\t" + <id> + "\n"} * {@code "name" + "\t" + <name> + "\n"} * {@code "<attribute_name1>" + "\t" + <attribute_class1> + "\t" + <min_occur> + "\t" + <max_occur> + "\n" + <nillable <True|False>>} * . * . * . * * </pre> * * Geometry attributes have an extra token per line representing the crs * */ private static final TextReader<RevFeatureType> FEATURETYPE_READER = new TextReader<RevFeatureType>() { private SimpleFeatureTypeBuilder builder; private FeatureTypeFactory typeFactory; @Override protected RevFeatureType read(ObjectId id, BufferedReader reader, TYPE type) throws IOException { Preconditions.checkArgument(TYPE.FEATURETYPE.equals(type), "Wrong type: %s", type.name()); builder = new SimpleFeatureTypeBuilder(); typeFactory = builder.getFeatureTypeFactory(); String name = parseLine(requireLine(reader), "name"); SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); if (name.contains(":")) { int idx = name.lastIndexOf(':'); String namespace = name.substring(0, idx); String local = name.substring(idx + 1); builder.setName(new NameImpl(namespace, local)); } else { builder.setName(new NameImpl(name)); } String line; while ((line = reader.readLine()) != null) { builder.add(parseAttributeDescriptor(line)); } SimpleFeatureType sft = builder.buildFeatureType(); return RevFeatureTypeImpl.build(sft); } private AttributeDescriptor parseAttributeDescriptor(String line) { ArrayList<String> tokens = Lists.newArrayList(Splitter.on('\t').split(line)); Preconditions.checkArgument(tokens.size() == 5 || tokens.size() == 6, "Wrong attribute definition: %s", line); NameImpl name = new NameImpl(tokens.get(0)); Class<?> type; try { type = FieldType.valueOf(tokens.get(1)).getBinding(); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Wrong type: " + tokens.get(1)); } int min = Integer.parseInt(tokens.get(2)); int max = Integer.parseInt(tokens.get(3)); boolean nillable = Boolean.parseBoolean(tokens.get(4)); /* * Default values that are currently not encoded. */ boolean isIdentifiable = false; boolean isAbstract = false; List<Filter> restrictions = null; AttributeType superType = null; InternationalString description = null; Object defaultValue = null; AttributeType attributeType; AttributeDescriptor attributeDescriptor; if (Geometry.class.isAssignableFrom(type)) { String crsText = tokens.get(5); CoordinateReferenceSystem crs = CrsTextSerializer.deserialize(crsText); attributeType = typeFactory.createGeometryType(name, type, crs, isIdentifiable, isAbstract, restrictions, superType, description); attributeDescriptor = typeFactory.createGeometryDescriptor( (GeometryType) attributeType, name, min, max, nillable, defaultValue); } else { attributeType = typeFactory.createAttributeType(name, type, isIdentifiable, isAbstract, restrictions, superType, description); attributeDescriptor = typeFactory.createAttributeDescriptor(attributeType, name, min, max, nillable, defaultValue); } return attributeDescriptor; } }; /** * Tree reader. * <p> * Parses a tree in the format: * * <pre> * {@code "id" + "\t" + <id> + "\n"} * * {@code "BUCKET" + "\t" + <bucket_idx> + "\t" + <ObjectId> +"\t" + <bounds> "\n"} * or * {@code "REF" + "\t" + <ref_type> + "\t" + <ref_name> + "\t" + <ObjectId> + "\t" + <MetadataId> + "\t" + <bounds> + "\n"} * . * . * . * </pre> * */ private static final TextReader<RevTree> TREE_READER = new TextReader<RevTree>() { @Override protected RevTree read(ObjectId id, BufferedReader reader, TYPE type) throws IOException { Preconditions.checkArgument(TYPE.TREE.equals(type), "Wrong type: %s", type.name()); Builder<Node> features = ImmutableList.builder(); Builder<Node> trees = ImmutableList.builder(); TreeMap<Integer, Bucket> subtrees = Maps.newTreeMap(); long size = Long.parseLong(parseLine(requireLine(reader), "size")); int numTrees = Integer.parseInt(parseLine(requireLine(reader), "numtrees")); String line; while ((line = reader.readLine()) != null) { Preconditions.checkArgument(!line.isEmpty(), "Empty tree element definition"); ArrayList<String> tokens = Lists.newArrayList(Splitter.on('\t').split(line)); String nodeType = tokens.get(0); if (nodeType.equals(TextWriter.TreeNode.REF.name())) { Node entryRef = parseNodeLine(line); if (entryRef.getType().equals(TYPE.TREE)) { trees.add(entryRef); } else { features.add(entryRef); } } else if (nodeType.equals(TextWriter.TreeNode.BUCKET.name())) { Preconditions.checkArgument(tokens.size() == 4, "Wrong bucket definition: %s", line); Integer idx = Integer.parseInt(tokens.get(1)); ObjectId bucketId = ObjectId.valueOf(tokens.get(2)); Envelope bounds = parseBBox(tokens.get(3)); Bucket bucket = Bucket.create(bucketId, bounds); subtrees.put(idx, bucket); } else { throw new IllegalArgumentException("Wrong tree element definition: " + line); } } RevTree tree; if (subtrees.isEmpty()) { tree = RevTreeImpl.createLeafTree(id, size, features.build(), trees.build()); } else { tree = RevTreeImpl.createNodeTree(id, size, numTrees, subtrees); } return tree; } }; /** * Tag reader. * <p> * Parses a tag of the format: * * <pre> * {@code TAG\n} * {@code "name" + "\t" + <tagname> + "\n"} * {@code "commitid" + "\t" + <comitid> + "\n"} * {@code "message" + "\t" + <message> + "\n"} * {@code "tagger" + "\t" + <tagger name> + " " + <tagger email> + "\t" + <tagger> + "\t" + <tagger_timezone_offset> + "\n"} * </pre> * */ private static final TextReader<RevTag> TAG_READER = new TextReader<RevTag>() { @Override protected RevTag read(ObjectId id, BufferedReader reader, TYPE type) throws IOException { Preconditions.checkArgument(TYPE.TAG.equals(type), "Wrong type: %s", type.name()); String name = parseLine(requireLine(reader), "name"); String message = parseLine(requireLine(reader), "message"); String commitId = parseLine(requireLine(reader), "commitid"); RevPerson tagger = parsePerson(requireLine(reader)); RevTag tag = new RevTagImpl(id, name, ObjectId.valueOf(commitId), message, tagger); return tag; } private RevPerson parsePerson(String line) throws IOException { String[] tokens = line.split("\t"); String header = "tagger"; Preconditions.checkArgument(header.equals(tokens[0]), "Expected field %s, got '%s'", header, tokens[0]); String name = tokens[1].trim().isEmpty() ? null : tokens[1]; String email = tokens[2].trim().isEmpty() ? null : tokens[2]; long timestamp = Long.parseLong(tokens[3]); int offset = Integer.parseInt(tokens[4]); return new RevPersonImpl(name, email, timestamp, offset); } }; private static String requireLine(BufferedReader reader) throws IOException { String line = reader.readLine(); if (line == null) { throw new IllegalStateException("Expected line bug got EOF"); } return line; } }