/* Copyright (c) 2012-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.api.plumbing; import java.io.IOException; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Map.Entry; import java.util.UUID; import org.geotools.referencing.CRS; 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.RevPerson; import org.locationtech.geogig.api.RevTag; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.storage.FieldType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.feature.type.PropertyType; 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.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.hash.Funnel; import com.google.common.hash.Funnels; import com.google.common.hash.PrimitiveSink; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateFilter; import com.vividsolutions.jts.geom.Geometry; /** * Hashes a RevObject and returns the ObjectId. * * @see RevObject * @see ObjectId */ class HashObjectFunnels { // This random byte code is used to represent null in hashing. This is intended to be something // that would be unlikely to duplicated by accident with real data. Changing this will cause all // objects that contain null values to hash differently. private static final byte[] NULL_BYTE_CODE = { 0x60, (byte) 0xe5, 0x6d, 0x08, (byte) 0xd3, 0x08, 0x53, (byte) 0xb7, (byte) 0x84, 0x07, 0x77 }; public static Funnel<RevCommit> commitFunnel() { return CommitFunnel.INSTANCE; } public static Funnel<RevTree> treeFunnel() { return TreeFunnel.INSTANCE; } public static Funnel<RevFeature> featureFunnel() { return FeatureFunnel.INSTANCE; } public static Funnel<RevTag> tagFunnel() { return TagFunnel.INSTANCE; } public static Funnel<RevFeatureType> featureTypeFunnel() { return FeatureTypeFunnel.INSTANCE; } private static final class NullableFunnel<T> implements Funnel<T> { private static final long serialVersionUID = -1L; private Funnel<T> nonNullableFunnel; /** * @param nonNullableFunnel */ public NullableFunnel(Funnel<T> nonNullableFunnel) { this.nonNullableFunnel = nonNullableFunnel; } @Override public void funnel(T from, PrimitiveSink into) { if (from == null) { NullFunnel.funnel(from, into); } else { nonNullableFunnel.funnel(from, into); } } public static <T> Funnel<T> of(Funnel<T> nonNullableFunnel) { return new NullableFunnel<T>(nonNullableFunnel); } } private static final Funnel<Object> NullFunnel = new Funnel<Object>() { private static final long serialVersionUID = 1L; @Override public void funnel(Object from, PrimitiveSink into) { Funnels.byteArrayFunnel().funnel(NULL_BYTE_CODE, into); } }; private static final Funnel<CharSequence> StringFunnel = Funnels.unencodedCharsFunnel(); private static final Funnel<CharSequence> NullableStringFunnel = NullableFunnel .of(StringFunnel); private static final Funnel<RevObject.TYPE> RevObjectTypeFunnel = new Funnel<RevObject.TYPE>() { private static final long serialVersionUID = 1L; @Override public void funnel(RevObject.TYPE from, PrimitiveSink into) { Funnels.integerFunnel().funnel(from.value(), into); } }; private static final Funnel<ObjectId> ObjectIdFunnel = new Funnel<ObjectId>() { private static final long serialVersionUID = 1L; @Override public void funnel(ObjectId from, PrimitiveSink into) { Funnels.byteArrayFunnel().funnel(from.getRawValue(), into); } }; private static final class CommitFunnel implements Funnel<RevCommit> { private static final long serialVersionUID = -1L; private static final CommitFunnel INSTANCE = new CommitFunnel(); @Override public void funnel(RevCommit from, PrimitiveSink into) { RevObjectTypeFunnel.funnel(TYPE.COMMIT, into); ObjectIdFunnel.funnel(from.getId(), into); ObjectIdFunnel.funnel(from.getTreeId(), into); for (ObjectId parentId : from.getParentIds()) { ObjectIdFunnel.funnel(parentId, into); } NullableStringFunnel.funnel(from.getMessage(), into); PersonFunnel.funnel(from.getAuthor(), into); PersonFunnel.funnel(from.getCommitter(), into); } }; private static final class TreeFunnel implements Funnel<RevTree> { private static final long serialVersionUID = 1L; private static final TreeFunnel INSTANCE = new TreeFunnel(); @Override public void funnel(RevTree from, PrimitiveSink into) { RevObjectTypeFunnel.funnel(TYPE.TREE, into); if (from.trees().isPresent()) { ImmutableList<Node> trees = from.trees().get(); Node ref; for (int i = 0; i < trees.size(); i++) { ref = trees.get(i); NodeFunnel.funnel(ref, into); } } if (from.features().isPresent()) { ImmutableList<Node> children = from.features().get(); Node ref; for (int i = 0; i < children.size(); i++) { ref = children.get(i); NodeFunnel.funnel(ref, into); } } if (from.buckets().isPresent()) { ImmutableSortedMap<Integer, Bucket> buckets = from.buckets().get(); for (Entry<Integer, Bucket> entry : buckets.entrySet()) { Funnels.integerFunnel().funnel(entry.getKey(), into); ObjectIdFunnel.funnel(entry.getValue().id(), into); } } } }; private static final class FeatureFunnel implements Funnel<RevFeature> { private static final long serialVersionUID = 1L; private static final FeatureFunnel INSTANCE = new FeatureFunnel(); @Override public void funnel(RevFeature from, PrimitiveSink into) { RevObjectTypeFunnel.funnel(TYPE.FEATURE, into); for (Optional<Object> value : from.getValues()) { PropertyValueFunnel.funnel(value.orNull(), into); } } }; private static final class FeatureTypeFunnel implements Funnel<RevFeatureType> { private static final long serialVersionUID = 1L; public static final FeatureTypeFunnel INSTANCE = new FeatureTypeFunnel(); @Override public void funnel(RevFeatureType from, PrimitiveSink into) { RevObjectTypeFunnel.funnel(TYPE.FEATURETYPE, into); ImmutableSet<PropertyDescriptor> featureTypeProperties = new DescribeFeatureType() .setFeatureType(from).call(); NameFunnel.funnel(from.getName(), into); for (PropertyDescriptor descriptor : featureTypeProperties) { PropertyDescriptorFunnel.funnel(descriptor, into); } } }; private static final class TagFunnel implements Funnel<RevTag> { private static final long serialVersionUID = 1L; public static final TagFunnel INSTANCE = new TagFunnel(); @Override public void funnel(RevTag from, PrimitiveSink into) { RevObjectTypeFunnel.funnel(TYPE.TAG, into); ObjectIdFunnel.funnel(from.getCommitId(), into); StringFunnel.funnel((CharSequence) from.getName(), into); StringFunnel.funnel((CharSequence) from.getMessage(), into); PersonFunnel.funnel(from.getTagger(), into); } }; private static final Funnel<RevPerson> PersonFunnel = NullableFunnel .of(new Funnel<RevPerson>() { private static final long serialVersionUID = -1L; @Override public void funnel(RevPerson from, PrimitiveSink into) { NullableStringFunnel.funnel(from.getName().orNull(), into); NullableStringFunnel.funnel(from.getEmail().orNull(), into); Funnels.longFunnel().funnel(from.getTimestamp(), into); Funnels.integerFunnel().funnel(from.getTimeZoneOffset(), into); } }); private static final Funnel<Node> NodeFunnel = new Funnel<Node>() { private static final long serialVersionUID = 1L; @Override public void funnel(Node ref, PrimitiveSink into) { RevObjectTypeFunnel.funnel(ref.getType(), into); StringFunnel.funnel((CharSequence) ref.getName(), into); ObjectIdFunnel.funnel(ref.getObjectId(), into); ObjectIdFunnel.funnel(ref.getMetadataId().or(ObjectId.NULL), into); } }; private static final Funnel<Object> PropertyValueFunnel = new Funnel<Object>() { private static final long serialVersionUID = 1L; @Override public void funnel(final Object value, PrimitiveSink into) { if (value == null) { NullFunnel.funnel(value, into); } else if (value instanceof String) { StringFunnel.funnel((CharSequence) value, into); } else if (value instanceof Boolean) { into.putBoolean(((Boolean) value).booleanValue()); } else if (value instanceof Byte) { into.putByte(((Byte) value).byteValue()); } else if (value instanceof Double) { into.putDouble(((Double) value).doubleValue()); } else if (value instanceof BigDecimal) { String bdString = ((BigDecimal) value).toEngineeringString(); StringFunnel.funnel(bdString, into); } else if (value instanceof Float) { into.putFloat(((Float) value).floatValue()); } else if (value instanceof Integer) { into.putInt(((Integer) value).intValue()); } else if (value instanceof BigInteger) { byte[] bigBytes = ((BigInteger) value).toByteArray(); into.putBytes(bigBytes); } else if (value instanceof Long) { into.putLong(((Long) value).longValue()); } else if (value.getClass().isArray()) { int length = Array.getLength(value); into.putInt(length); for (int i = 0; i < length; i++) { Object arrayElem = Array.get(value, i); PropertyValueFunnel.funnel(arrayElem, into); } } else if (value instanceof java.util.UUID) { UUID uuid = (UUID) value; long most = uuid.getMostSignificantBits(); long least = uuid.getLeastSignificantBits(); into.putLong(most); into.putLong(least); } else if (value instanceof Geometry) { GeometryFunnel.funnel((Geometry) value, into); } else if (value instanceof Serializable) { OutputStream byteOutput = Funnels.asOutputStream(into); try { ObjectOutput objectOut = new ObjectOutputStream(byteOutput); objectOut.writeObject(value); objectOut.close(); byteOutput.close(); } catch (IOException shouldntHappen) { throw Throwables.propagate(shouldntHappen); } } else { StringFunnel.funnel((CharSequence) value.getClass().getName(), into); StringFunnel.funnel((CharSequence) value.toString(), into); } } }; /** * Rounds geometry ordinates to 9 decimals before hashing them */ private static final Funnel<Geometry> GeometryFunnel = new Funnel<Geometry>() { private static final long serialVersionUID = 1L; @Override public void funnel(final Geometry geom, final PrimitiveSink into) { CoordinateFilter filter = new CoordinateFilter() { final double scale = 1E9D; @Override public void filter(Coordinate coord) { double x = Math.round(coord.x * scale) / scale; double y = Math.round(coord.y * scale) / scale; into.putDouble(x); into.putDouble(y); } }; geom.apply(filter); } }; private static final Funnel<Name> NameFunnel = new Funnel<Name>() { private static final long serialVersionUID = 1L; @Override public void funnel(Name from, PrimitiveSink into) { NullableStringFunnel.funnel(from.getNamespaceURI(), into); StringFunnel.funnel((CharSequence) from.getLocalPart(), into); } }; private static final Funnel<PropertyDescriptor> PropertyDescriptorFunnel = new Funnel<PropertyDescriptor>() { private static final long serialVersionUID = 1L; @Override public void funnel(PropertyDescriptor descriptor, PrimitiveSink into) { NameFunnel.funnel(descriptor.getName(), into); PropertyType attrType = descriptor.getType(); NameFunnel.funnel(attrType.getName(), into); FieldType type = FieldType.forBinding(attrType.getBinding()); into.putInt(type.getTextTag()); into.putBoolean(descriptor.isNillable()); into.putInt(descriptor.getMaxOccurs()); into.putInt(descriptor.getMinOccurs()); if (descriptor instanceof GeometryDescriptor) { CoordinateReferenceSystem crs; crs = ((GeometryDescriptor) descriptor).getCoordinateReferenceSystem(); String srsName; if (crs == null) { srsName = "urn:ogc:def:crs:EPSG::0"; } else { srsName = CRS.toSRS(crs); } NullableStringFunnel.funnel(srsName, into); } } }; }