/* Copyright (c) 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: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.storage.datastream; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.COMMIT_AUTHOR_PREFIX; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.COMMIT_COMMITTER_PREFIX; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.COMMIT_PARENT_REF; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.COMMIT_TREE_REF; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.NUL; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readCommit; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readFeature; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readFeatureType; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readTag; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readToMarker; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.readTree; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.requireHeader; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.writeBucket; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.writeHeader; import static org.locationtech.geogig.storage.datastream.FormatCommonV1.writeNode; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.EnumMap; import java.util.Map; import org.geotools.referencing.CRS; import org.geotools.referencing.CRS.AxisOrder; import org.geotools.referencing.wkt.Formattable; import org.locationtech.geogig.api.Bucket; 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.RevFeatureType; import org.locationtech.geogig.api.RevObject; import org.locationtech.geogig.api.RevObject.TYPE; import org.locationtech.geogig.api.RevTag; import org.locationtech.geogig.api.RevTree; 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.type.GeometryType; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.feature.type.PropertyType; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Maps; import com.vividsolutions.jts.geom.Envelope; public class DataStreamSerializationFactoryV1 implements ObjectSerializingFactory { /** * factory singleton */ public static final DataStreamSerializationFactoryV1 INSTANCE = new DataStreamSerializationFactoryV1(); private final static ObjectReader<RevObject> OBJECT_READER = new ObjectReaderV1(); private static final EnumMap<TYPE, Serializer<? extends RevObject>> serializers = Maps .newEnumMap(TYPE.class); static { serializers.put(TYPE.COMMIT, new CommitSerializer()); serializers.put(TYPE.FEATURE, new FeatureSerializer()); serializers.put(TYPE.FEATURETYPE, new FeatureTypeSerializer()); serializers.put(TYPE.TAG, new TagSerializer()); serializers.put(TYPE.TREE, new TreeSerializer()); } @SuppressWarnings("unchecked") private static <T extends RevObject> Serializer<T> serializer(TYPE type) { Serializer<? extends RevObject> serializer = serializers.get(type); if (serializer == null) { throw new UnsupportedOperationException("No serializer for " + type); } return (Serializer<T>) serializer; } @Override public ObjectReader<RevCommit> createCommitReader() { return serializer(TYPE.COMMIT); } @Override public ObjectReader<RevTree> createRevTreeReader() { return serializer(TYPE.TREE); } @Override public ObjectReader<RevFeature> createFeatureReader() { return serializer(TYPE.FEATURE); } @Override public ObjectReader<RevFeature> createFeatureReader(Map<String, Serializable> hints) { return serializer(TYPE.FEATURE); } @Override public ObjectReader<RevFeatureType> createFeatureTypeReader() { return serializer(TYPE.FEATURETYPE); } @Override public <T extends RevObject> ObjectWriter<T> createObjectWriter(TYPE type) { return serializer(type); } @Override public <T extends RevObject> ObjectReader<T> createObjectReader(TYPE type) { return serializer(type); } @Override public ObjectReader<RevObject> createObjectReader() { return OBJECT_READER; } private static interface Serializer<T extends RevObject> extends ObjectReader<T>, ObjectWriter<T> { // } private static class CommitSerializer implements Serializer<RevCommit> { @Override public RevCommit read(ObjectId id, InputStream rawData) throws IllegalArgumentException { DataInput in = new DataInputStream(rawData); try { requireHeader(in, "commit"); return readCommit(id, in); } catch (IOException e) { Throwables.propagate(e); } throw new IllegalStateException( "Unexpected state: neither succeeded nor threw exception while trying to read commit " + id); } @Override public void write(RevCommit commit, OutputStream out) throws IOException { DataOutput data = new DataOutputStream(out); FormatCommonV1.writeHeader(data, "commit"); data.writeByte(COMMIT_TREE_REF); data.write(commit.getTreeId().getRawValue()); for (ObjectId pId : commit.getParentIds()) { data.writeByte(COMMIT_PARENT_REF); data.write(pId.getRawValue()); } data.writeByte(COMMIT_AUTHOR_PREFIX); FormatCommonV1.writePerson(commit.getAuthor(), data); data.writeByte(COMMIT_COMMITTER_PREFIX); FormatCommonV1.writePerson(commit.getCommitter(), data); data.writeUTF(commit.getMessage()); } } private static class FeatureSerializer implements Serializer<RevFeature> { @Override public RevFeature read(ObjectId id, InputStream rawData) throws IllegalArgumentException { DataInput in = new DataInputStream(rawData); try { requireHeader(in, "feature"); return readFeature(id, in); } catch (IOException e) { Throwables.propagate(e); } throw new IllegalStateException( "Didn't expect to reach end of FeatureReader.read(); We should have returned or thrown an error before this point."); } @Override public void write(RevFeature feature, OutputStream out) throws IOException { DataOutput data = new DataOutputStream(out); writeHeader(data, "feature"); data.writeInt(feature.getValues().size()); for (Optional<Object> field : feature.getValues()) { FieldType type = FieldType.forValue(field); data.writeByte(type.getTag()); if (type != FieldType.NULL) { DataStreamValueSerializerV1.write(field, data); } } } } private static class FeatureTypeSerializer implements Serializer<RevFeatureType> { @Override public RevFeatureType read(ObjectId id, InputStream rawData) throws IllegalArgumentException { DataInput in = new DataInputStream(rawData); try { requireHeader(in, "featuretype"); return readFeatureType(id, in); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void write(RevFeatureType object, OutputStream out) throws IOException { DataOutput data = new DataOutputStream(out); writeHeader(data, "featuretype"); writeName(object.getName(), data); data.writeInt(object.sortedDescriptors().size()); for (PropertyDescriptor desc : object.type().getDescriptors()) { writeProperty(desc, data); } } private void writeName(Name name, DataOutput data) throws IOException { final String ns = name.getNamespaceURI(); final String lp = name.getLocalPart(); data.writeUTF(ns == null ? "" : ns); data.writeUTF(lp == null ? "" : lp); } private void writePropertyType(PropertyType type, DataOutput data) throws IOException { writeName(type.getName(), data); data.writeByte(FieldType.forBinding(type.getBinding()).getTag()); if (type instanceof GeometryType) { GeometryType gType = (GeometryType) type; CoordinateReferenceSystem crs = gType.getCoordinateReferenceSystem(); String srsName; if (crs == null) { srsName = "urn:ogc:def:crs:EPSG::0"; } else { final boolean longitudeFirst = CRS.getAxisOrder(crs, false) == AxisOrder.EAST_NORTH; final boolean codeOnly = true; String crsCode = CRS.toSRS(crs, codeOnly); if (crsCode != null) { srsName = (longitudeFirst ? "EPSG:" : "urn:ogc:def:crs:EPSG::") + crsCode; // check that what we are writing is actually a valid EPSG code and we will // be // able to decode it later. If not, we will use WKT instead try { CRS.decode(srsName, longitudeFirst); } catch (NoSuchAuthorityCodeException e) { srsName = null; } catch (FactoryException e) { srsName = null; } } else { srsName = null; } } if (srsName != null) { data.writeBoolean(true); data.writeUTF(srsName); } else { final String wkt; if (crs instanceof Formattable) { wkt = ((Formattable) crs).toWKT(Formattable.SINGLE_LINE); } else { wkt = crs.toWKT(); } data.writeBoolean(false); data.writeUTF(wkt); } } } private void writeProperty(PropertyDescriptor attr, DataOutput data) throws IOException { writeName(attr.getName(), data); data.writeBoolean(attr.isNillable()); data.writeInt(attr.getMinOccurs()); data.writeInt(attr.getMaxOccurs()); writePropertyType(attr.getType(), data); } } private static class TagSerializer implements Serializer<RevTag> { public RevTag read(ObjectId id, InputStream in) { DataInput data = new DataInputStream(in); try { FormatCommonV1.requireHeader(data, "tag"); return FormatCommonV1.readTag(id, data); } catch (Exception e) { throw Throwables.propagate(e); } } public void write(RevTag tag, OutputStream out) { final DataOutput data = new DataOutputStream(out); try { FormatCommonV1.writeHeader(data, "tag"); FormatCommonV1.writeTag(tag, data); } catch (Exception e) { throw Throwables.propagate(e); } } } private static class TreeSerializer implements Serializer<RevTree> { @Override public RevTree read(ObjectId id, InputStream rawData) throws IllegalArgumentException { DataInput in = new DataInputStream(rawData); try { requireHeader(in, "tree"); return readTree(id, in); } catch (IOException e) { Throwables.propagate(e); } throw new IllegalStateException( "Unexpected state: neither succeeded nor threw exception while trying to read commit " + id); } @Override public void write(RevTree tree, OutputStream out) throws IOException { DataOutput data = new DataOutputStream(out); writeHeader(data, "tree"); data.writeLong(tree.size()); data.writeInt(tree.numTrees()); Envelope envBuff = new Envelope(); if (tree.features().isPresent()) { data.writeInt(tree.features().get().size()); ImmutableList<Node> features = tree.features().get(); for (Node feature : features) { writeNode(feature, data, envBuff); } } else { data.writeInt(0); } if (tree.trees().isPresent()) { data.writeInt(tree.trees().get().size()); ImmutableList<Node> subTrees = tree.trees().get(); for (Node subTree : subTrees) { writeNode(subTree, data, envBuff); } } else { data.writeInt(0); } if (tree.buckets().isPresent()) { data.writeInt(tree.buckets().get().size()); ImmutableSortedMap<Integer, Bucket> buckets = tree.buckets().get(); for (Map.Entry<Integer, Bucket> bucket : buckets.entrySet()) { writeBucket(bucket.getKey(), bucket.getValue(), data, envBuff); } } else { data.writeInt(0); } } } private static class ObjectReaderV1 implements org.locationtech.geogig.storage.ObjectReader<RevObject> { @Override public RevObject read(ObjectId id, InputStream rawData) throws IllegalArgumentException { DataInput in = new DataInputStream(rawData); try { return readData(id, in); } catch (IOException e) { throw new RuntimeException(e); } } private RevObject readData(ObjectId id, DataInput in) throws IOException { String header = readToMarker(in, NUL); if ("commit".equals(header)) return readCommit(id, in); else if ("tree".equals(header)) return readTree(id, in); else if ("feature".equals(header)) return readFeature(id, in); else if ("featuretype".equals(header)) return readFeatureType(id, in); else if ("tag".equals(header)) return readTag(id, in); else throw new IllegalArgumentException("Unrecognized object header: " + header); } } }