/* 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 java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.annotation.Nullable;
import org.geotools.feature.NameImpl;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.feature.type.BasicFeatureTypes;
import org.geotools.referencing.CRS;
import org.locationtech.geogig.api.Bucket;
import org.locationtech.geogig.api.Node;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevCommitImpl;
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.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.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.storage.FieldType;
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.Name;
import org.opengis.filter.Filter;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
public class FormatCommonV1 {
public final static byte NUL = 0x00;
public final static String readToMarker(DataInput in, byte marker) throws IOException {
StringBuilder buff = new StringBuilder();
byte b = in.readByte();
while (b != marker) {
buff.append((char) b);
b = in.readByte();
}
return buff.toString();
}
public final static void requireHeader(DataInput in, String header) throws IOException {
String s = readToMarker(in, NUL);
if (!header.equals(s))
throw new IllegalArgumentException("Expected header " + header + ", but actually got "
+ s);
}
public final static ObjectId readObjectId(DataInput in) throws IOException {
byte[] bytes = new byte[ObjectId.NUM_BYTES];
in.readFully(bytes);
return ObjectId.createNoClone(bytes);
}
public static final byte COMMIT_TREE_REF = 0x01;
public static final byte COMMIT_PARENT_REF = 0x02;
public static final byte COMMIT_AUTHOR_PREFIX = 0x03;
public static final byte COMMIT_COMMITTER_PREFIX = 0x04;
/**
* Constant for reading TREE objects. Indicates that the end of the tree object has been
* reached.
*/
public static final byte NO_MORE_NODES = 0x00;
/**
* Constant for reading TREE objects. Indicates that the next entry is a subtree node or a
* features node.
*/
public static final byte NODE = 0x01;
/**
* Constant for reading TREE objects. Indicates that the next entry is a bucket.
*/
public static final byte BUCKET = 0x02;
/**
* The featuretype factory to use when calling code does not provide one.
*/
private static final FeatureTypeFactory DEFAULT_FEATURETYPE_FACTORY = new SimpleFeatureTypeBuilder()
.getFeatureTypeFactory();
public static RevTag readTag(ObjectId id, DataInput in) throws IOException {
final ObjectId commitId = readObjectId(in);
final String name = in.readUTF();
final String message = in.readUTF();
final RevPerson tagger = readRevPerson(in);
return new RevTagImpl(id, name, commitId, message, tagger);
}
public static void writeTag(RevTag tag, DataOutput out) throws IOException {
out.write(tag.getCommitId().getRawValue());
out.writeUTF(tag.getName());
out.writeUTF(tag.getMessage());
writePerson(tag.getTagger(), out);
}
public static RevCommit readCommit(ObjectId id, DataInput in) throws IOException {
byte tag = in.readByte();
if (tag != COMMIT_TREE_REF) {
throw new IllegalArgumentException("Commit should include a tree ref");
}
final byte[] treeIdBytes = new byte[20];
in.readFully(treeIdBytes);
final ObjectId treeId = ObjectId.createNoClone(treeIdBytes);
final Builder<ObjectId> parentListBuilder = ImmutableList.builder();
while (true) {
tag = in.readByte();
if (tag != COMMIT_PARENT_REF) {
break;
} else {
final byte[] parentIdBytes = new byte[20];
in.readFully(parentIdBytes);
parentListBuilder.add(ObjectId.createNoClone(parentIdBytes));
}
}
if (tag != COMMIT_AUTHOR_PREFIX) {
throw new IllegalArgumentException(
"Expected AUTHOR element following parent ids in commit");
}
final RevPerson author = readRevPerson(in);
tag = in.readByte();
if (tag != COMMIT_COMMITTER_PREFIX) {
throw new IllegalArgumentException(
"Expected COMMITTER element following author in commit");
}
final RevPerson committer = readRevPerson(in);
final String message = in.readUTF();
return new RevCommitImpl(id, treeId, parentListBuilder.build(), author, committer, message);
}
public static final RevPerson readRevPerson(DataInput in) throws IOException {
final String name = in.readUTF();
final String email = in.readUTF();
final long timestamp = in.readLong();
final int tzOffset = in.readInt();
return new RevPersonImpl(name.length() == 0 ? null : name, email.length() == 0 ? null
: email, timestamp, tzOffset);
}
public static final void writePerson(RevPerson person, DataOutput data) throws IOException {
data.writeUTF(person.getName().or(""));
data.writeUTF(person.getEmail().or(""));
data.writeLong(person.getTimestamp());
data.writeInt(person.getTimeZoneOffset());
}
public static RevTree readTree(ObjectId id, DataInput in) throws IOException {
final long size = in.readLong();
final int treeCount = in.readInt();
final ImmutableList.Builder<Node> featuresBuilder = new ImmutableList.Builder<Node>();
final ImmutableList.Builder<Node> treesBuilder = new ImmutableList.Builder<Node>();
final SortedMap<Integer, Bucket> buckets = new TreeMap<Integer, Bucket>();
final int nFeatures = in.readInt();
for (int i = 0; i < nFeatures; i++) {
Node n = readNode(in);
if (n.getType() != RevObject.TYPE.FEATURE) {
throw new IllegalStateException("Non-feature node in tree's feature list.");
}
featuresBuilder.add(n);
}
final int nTrees = in.readInt();
for (int i = 0; i < nTrees; i++) {
Node n = readNode(in);
if (n.getType() != RevObject.TYPE.TREE) {
throw new IllegalStateException("Non-tree node in tree's subtree list.");
}
treesBuilder.add(n);
}
final int nBuckets = in.readInt();
for (int i = 0; i < nBuckets; i++) {
int key = in.readInt();
Bucket bucket = readBucket(in);
buckets.put(key, bucket);
}
ImmutableList<Node> trees = treesBuilder.build();
ImmutableList<Node> features = featuresBuilder.build();
if (nTrees == 0 && nFeatures == 0 && nBuckets == 0) {
return RevTree.EMPTY;
} else if (trees.isEmpty() && features.isEmpty()) {
return RevTreeImpl.createNodeTree(id, size, treeCount, buckets);
} else if (buckets.isEmpty()) {
return RevTreeImpl.createLeafTree(id, size, features, trees);
} else {
throw new IllegalArgumentException(
"Tree has mixed buckets and nodes; this is not supported.");
}
}
public static Node readNode(DataInput in) throws IOException {
final String name = in.readUTF();
final byte[] objectId = new byte[20];
in.readFully(objectId);
final byte[] metadataId = new byte[20];
in.readFully(metadataId);
final RevObject.TYPE contentType = RevObject.TYPE.valueOf(in.readByte());
final Envelope bbox = readBBox(in);
final Node node;
node = Node.create(name, ObjectId.createNoClone(objectId),
ObjectId.createNoClone(metadataId), contentType, bbox);
return node;
}
public static DiffEntry readDiff(DataInput in) throws IOException {
boolean oldNode = in.readBoolean();
NodeRef oldNodeRef = null;
if (oldNode) {
oldNodeRef = readNodeRef(in);
}
boolean newNode = in.readBoolean();
NodeRef newNodeRef = null;
if (newNode) {
newNodeRef = readNodeRef(in);
}
return new DiffEntry(oldNodeRef, newNodeRef);
}
public static NodeRef readNodeRef(DataInput in) throws IOException {
Node node = readNode(in);
final byte[] metadataId = new byte[20];
in.readFully(metadataId);
String parentPath = in.readUTF();
return new NodeRef(node, parentPath, ObjectId.createNoClone(metadataId));
}
public static final Bucket readBucket(DataInput in) throws IOException {
final byte[] hash = new byte[20];
in.readFully(hash);
ObjectId objectId = ObjectId.createNoClone(hash);
Envelope bounds = readBBox(in);
return Bucket.create(objectId, bounds);
}
@Nullable
private static Envelope readBBox(DataInput in) throws IOException {
final double minx = in.readDouble();
if (Double.isNaN(minx)) {
return null;
}
final double maxx = in.readDouble();
final double miny = in.readDouble();
final double maxy = in.readDouble();
return new Envelope(minx, maxx, miny, maxy);
}
public static RevFeature readFeature(ObjectId id, DataInput in) throws IOException {
final int count = in.readInt();
final ImmutableList.Builder<Optional<Object>> builder = ImmutableList.builder();
for (int i = 0; i < count; i++) {
final byte fieldTag = in.readByte();
final FieldType fieldType = FieldType.valueOf(fieldTag);
Object value = DataStreamValueSerializerV1.read(fieldType, in);
builder.add(Optional.fromNullable(value));
}
return new RevFeatureImpl(id, builder.build());
}
public static RevFeatureType readFeatureType(ObjectId id, DataInput in) throws IOException {
return readFeatureType(id, in, DEFAULT_FEATURETYPE_FACTORY);
}
public static RevFeatureType readFeatureType(ObjectId id, DataInput in,
FeatureTypeFactory typeFactory) throws IOException {
Name name = readName(in);
int propertyCount = in.readInt();
List<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
for (int i = 0; i < propertyCount; i++) {
attributes.add(readAttributeDescriptor(in, typeFactory));
}
SimpleFeatureType ftype = typeFactory.createSimpleFeatureType(name, attributes, null,
false, Collections.<Filter> emptyList(), BasicFeatureTypes.FEATURE, null);
return new RevFeatureTypeImpl(id, ftype);
}
private static Name readName(DataInput in) throws IOException {
String namespace = in.readUTF();
String localPart = in.readUTF();
return new NameImpl(namespace.length() == 0 ? null : namespace,
localPart.length() == 0 ? null : localPart);
}
private static AttributeType readAttributeType(DataInput in, FeatureTypeFactory typeFactory)
throws IOException {
final Name name = readName(in);
final byte typeTag = in.readByte();
final FieldType type = FieldType.valueOf(typeTag);
if (Geometry.class.isAssignableFrom(type.getBinding())) {
final boolean isCRSCode = in.readBoolean(); // as opposed to a raw
// WKT string
final String crsText = in.readUTF();
final CoordinateReferenceSystem crs;
try {
if (isCRSCode) {
if ("urn:ogc:def:crs:EPSG::0".equals(crsText)) {
crs = null;
} else {
boolean forceLongitudeFirst = crsText.startsWith("EPSG:");
crs = CRS.decode(crsText, forceLongitudeFirst);
}
} else {
crs = CRS.parseWKT(crsText);
}
} catch (FactoryException e) {
throw new RuntimeException(e);
}
return typeFactory.createGeometryType(name, type.getBinding(), crs, false, false,
Collections.<Filter> emptyList(), null, null);
} else {
return typeFactory.createAttributeType(name, type.getBinding(), false, false,
Collections.<Filter> emptyList(), null, null);
}
}
private static AttributeDescriptor readAttributeDescriptor(DataInput in,
FeatureTypeFactory typeFactory) throws IOException {
final Name name = readName(in);
final boolean nillable = in.readBoolean();
final int minOccurs = in.readInt();
final int maxOccurs = in.readInt();
final AttributeType type = readAttributeType(in, typeFactory);
if (type instanceof GeometryType)
return typeFactory.createGeometryDescriptor((GeometryType) type, name, minOccurs,
maxOccurs, nillable, null);
else
return typeFactory.createAttributeDescriptor(type, name, minOccurs, maxOccurs,
nillable, null);
}
public static void writeHeader(DataOutput data, String header) throws IOException {
byte[] bytes = header.getBytes(Charset.forName("US-ASCII"));
data.write(bytes);
data.writeByte(NUL);
}
public static void writeBoundingBox(Envelope bbox, DataOutput data) throws IOException {
if (bbox.isNull()) {
data.writeDouble(Double.NaN);
} else {
data.writeDouble(bbox.getMinX());
data.writeDouble(bbox.getMaxX());
data.writeDouble(bbox.getMinY());
data.writeDouble(bbox.getMaxY());
}
}
public static void writeBucket(int index, Bucket bucket, DataOutput data) throws IOException {
writeBucket(index, bucket, data, new Envelope());
}
public static void writeBucket(int index, Bucket bucket, DataOutput data, Envelope envBuff)
throws IOException {
data.writeInt(index);
data.write(bucket.id().getRawValue());
envBuff.setToNull();
bucket.expand(envBuff);
writeBoundingBox(envBuff, data);
}
public static void writeNode(Node node, DataOutput data) throws IOException {
writeNode(node, data, new Envelope());
}
public static void writeNode(Node node, DataOutput data, Envelope envBuff) throws IOException {
data.writeUTF(node.getName());
data.write(node.getObjectId().getRawValue());
data.write(node.getMetadataId().or(ObjectId.NULL).getRawValue());
int typeN = node.getType().value();
data.writeByte(typeN);
envBuff.setToNull();
node.expand(envBuff);
writeBoundingBox(envBuff, data);
}
public static void writeDiff(DiffEntry diff, DataOutput data) throws IOException {
if (diff.getOldObject() == null) {
data.writeBoolean(false);
} else {
data.writeBoolean(true);
writeNodeRef(diff.getOldObject(), data);
}
if (diff.getNewObject() == null) {
data.writeBoolean(false);
} else {
data.writeBoolean(true);
writeNodeRef(diff.getNewObject(), data);
}
}
public static void writeNodeRef(NodeRef nodeRef, DataOutput data) throws IOException {
writeNode(nodeRef.getNode(), data);
data.write(nodeRef.getMetadataId().getRawValue());
data.writeUTF(nodeRef.getParentPath());
}
}