/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geoserver.data.versioning.decorator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;
import org.geogit.api.DiffEntry;
import org.geogit.api.DiffOp;
import org.geogit.api.GeoGIT;
import org.geogit.api.LogOp;
import org.geogit.api.ObjectId;
import org.geogit.api.Ref;
import org.geogit.api.RevCommit;
import org.geogit.api.RevObject.TYPE;
import org.geogit.api.RevTree;
import org.geogit.repository.Repository;
import org.geogit.storage.StagingDatabase;
import org.geotools.data.Query;
import org.geotools.util.DateRange;
import org.geotools.util.Range;
import org.opengis.feature.type.Name;
import org.opengis.filter.identity.ResourceId;
import org.opengis.filter.identity.Version;
import org.opengis.filter.identity.Version.Action;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterators;
public class VersionQuery {
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger("org.geoserver.data.geogit.decorator");
private final Name typeName;
private final GeoGIT ggit;
public VersionQuery(final GeoGIT ggit, final Name typeName) {
this.ggit = ggit;
this.typeName = typeName;
}
public Iterator<Ref> getByQuery(Query query) {
VersionDetail vDetail = VersionDetail.extractVersionDetails(query);
if (vDetail == null) {
return Iterators.emptyIterator();
}
VersionDetail.VersionType vType = vDetail.getType();
switch (vType) {
case Action:
return fetchByAction(vDetail.getAction()).iterator();
case Date:
return fetchByDate(vDetail.getDate()).iterator();
case DateRange:
return fetchByRange(vDetail.getRange()).iterator();
case Index:
return fetchByIndex(vDetail.getIndex()).iterator();
}
return Iterators.emptyIterator();
}
public Iterator<Ref> filterByQueryVersion(Iterator<Ref> refs, Query query) {
VersionDetail vDetail = VersionDetail.extractVersionDetails(query);
if (vDetail == null) {
return refs;
}
VersionDetail.VersionType vType = vDetail.getType();
switch (vType) {
case Action:
return filterByAction(refs, vDetail.getAction()).iterator();
case Date:
return filterByDate(refs, vDetail.getDate()).iterator();
case DateRange:
return filterByRange(refs, vDetail.getRange()).iterator();
case Index:
return filterByIndex(refs, vDetail.getIndex()).iterator();
}
return Iterators.emptyIterator();
}
private List<Ref> fetchByDate(final Date date) {
LogOp logOp = ggit.log().addPath(typeNamePath());
try {
Iterator<RevCommit> featureCommits = logOp.call();
RevCommit commit = findClosest(date, featureCommits);
return getRefsByCommit(commit);
} catch (Exception ex) {
/*
* Need some logging.
*/
return Collections.emptyList();
}
}
private List<Ref> filterByDate(Iterator<Ref> refs, final Date date) {
List<Ref> dateRefs = fetchByDate(date);
return filterIteratorByList(dateRefs, refs);
}
private List<Ref> fetchByRange(DateRange range) {
return null;
}
private List<Ref> filterByRange(Iterator<Ref> refs, DateRange range) {
List<Ref> rangeRefs = fetchByRange(range);
return filterIteratorByList(rangeRefs, refs);
}
private List<Ref> fetchByAction(Version.Action action) {
List<Ref> featureRefs = new ArrayList<Ref>();
if (Version.Action.ALL.equals(action)) {
LogOp logOp = ggit.log().addPath(typeNamePath());
try {
Iterator<RevCommit> featureCommits = logOp.call();
while (featureCommits.hasNext()) {
featureRefs.addAll(getRefsByCommit(featureCommits.next()));
}
} catch (Exception ex) {
return Collections.emptyList();
}
} else if (Version.Action.NEXT.equals(action)
|| Version.Action.PREVIOUS.equals(action)) {
/*
* There is no reference point for NEXT/PREVIOUS, so we're going to
* leave them with nothing.
*/
return featureRefs;
} else if (Version.Action.FIRST.equals(action)) {
featureRefs = fetchByIndex(1);
} else if (Version.Action.LAST.equals(action)) {
LogOp logOp = ggit.log().addPath(typeNamePath()).setLimit(1);
try {
Iterator<RevCommit> featureCommits = logOp.call();
if (featureCommits.hasNext()) {
RevCommit commit = featureCommits.next();
return getRefsByCommit(commit);
}
} catch (Exception ex) {
return Collections.emptyList();
}
}
return featureRefs;
}
private List<Ref> filterByAction(Iterator<Ref> refs, Version.Action action) {
if (Version.Action.ALL.equals(action)) {
List<Ref> newRefs = new ArrayList<Ref>();
while (refs.hasNext()) {
Ref ref = refs.next();
newRefs.add(ref);
}
return newRefs;
} else if (Version.Action.NEXT.equals(action)) {
// iterate through refs and retrieve the previous version
return Collections.emptyList();
} else if (Version.Action.PREVIOUS.equals(action)) {
// iterate through refs and retrieve the next version
return Collections.emptyList();
}
List<Ref> actionRefs = fetchByAction(action);
return filterIteratorByList(actionRefs, refs);
}
private List<Ref> fetchByIndex(int index) {
LogOp logOp = ggit.log().addPath(typeNamePath());
try {
Iterator<RevCommit> featureCommits = logOp.call();
RevCommit[] commitTrail = new RevCommit[index];
int ind = 0;
int count = 0;
RevCommit latest = null;
RevCommit target = null;
while (featureCommits.hasNext()) {
RevCommit commit = featureCommits.next();
if (latest == null) {
latest = commit;
}
commitTrail[ind] = commit;
ind = (ind + 1) % index;
count++;
}
if (count < index)
target = latest;
else
target = commitTrail[(ind + 1) % index];
return getRefsByCommit(target);
} catch (Exception ex) {
return Collections.emptyList();
}
}
private List<Ref> filterByIndex(Iterator<Ref> refs, int index) {
List<Ref> indexRefs = fetchByIndex(index);
return filterIteratorByList(indexRefs, refs);
}
private List<Ref> getRefsByCommit(RevCommit commit) {
List<Ref> treeRefs = new ArrayList<Ref>();
if (commit != null) {
ObjectId commitTreeId = commit.getTreeId();
RevTree commitTree = ggit.getRepository().getTree(commitTreeId);
Ref nsRef = commitTree.get(typeName.getNamespaceURI());
RevTree nsTree = ggit.getRepository().getTree(nsRef.getObjectId());
Ref typeRef = nsTree.get(typeName.getLocalPart());
RevTree typeTree = ggit.getRepository().getTree(
typeRef.getObjectId());
Iterator<Ref> it = typeTree.iterator(null);
while (it.hasNext()) {
Ref nextRef = it.next();
treeRefs.add(nextRef);
}
}
return treeRefs;
}
private List<Ref> filterIteratorByList(List<Ref> refList, Iterator<Ref> refs) {
Preconditions.checkNotNull(refs);
Preconditions.checkNotNull(refList);
List<Ref> newRefs = new ArrayList<Ref>();
while (refs.hasNext()) {
Ref ref = refs.next();
if (refList.contains(ref)) {
newRefs.add(ref);
}
}
return newRefs;
}
/**
* @param id
* @return an iterator for all the requested versions of a given feature, or
* the empty iterator if no such feature is found.
* @throws Exception
*/
public Iterator<Ref> get(final ResourceId id) throws Exception {
final String featureId = id.getID();
final String featureVersion = id.getFeatureVersion();
final Version version = id.getVersion();
final boolean isDateRangeQuery = id.getStartTime() != null
|| id.getEndTime() != null;
final boolean isVesionQuery = !version.isEmpty();
final Ref requestedVersionRef = extractRequestedVersion(ggit,
featureId, featureVersion);
{
final boolean explicitVersionQuery = !isDateRangeQuery
&& !isVesionQuery;
if (explicitVersionQuery) {
if (requestedVersionRef == null) {
return Iterators.emptyIterator();
} else {
// easy, no extra constraints specified
return Iterators.singletonIterator(requestedVersionRef);
}
}
}
// at this point is either a version query or a date range query...
List<Ref> result = new ArrayList<Ref>(5);
// filter commits that affect the requested feature
final List<String> path = path(featureId);
LogOp logOp = ggit.log().addPath(path);
if (isDateRangeQuery) {
// time range query, limit commits by time range, if speficied
Date startTime = id.getStartTime() == null ? new Date(0L) : id
.getStartTime();
Date endTime = id.getEndTime() == null ? new Date(Long.MAX_VALUE)
: id.getEndTime();
boolean isMinIncluded = true;
boolean isMaxIncluded = true;
Range<Date> timeRange = new Range<Date>(Date.class, startTime,
isMinIncluded, endTime, isMaxIncluded);
logOp.setTimeRange(timeRange);
}
// all commits whose tree contains the requested feature
Iterator<RevCommit> featureCommits = logOp.call();
if (isDateRangeQuery) {
List<Ref> allInAscendingOrder = getAllInAscendingOrder(ggit,
featureCommits, featureId);
result.addAll(allInAscendingOrder);
} else if (isVesionQuery) {
if (version.isDateTime()) {
final Date validAsOf = version.getDateTime();
RevCommit closest = findClosest(validAsOf, featureCommits);
if (closest != null) {
featureCommits = Iterators.singletonIterator(closest);
result.addAll(getAllInAscendingOrder(ggit, featureCommits,
featureId));
}
} else if (version.isIndex()) {
final int requestIndex = version.getIndex().intValue();
final int listIndex = requestIndex - 1;// version indexing
// starts at 1
List<Ref> allVersions = getAllInAscendingOrder(ggit,
featureCommits, featureId);
if (allVersions.size() > 0) {
if (allVersions.size() >= requestIndex) {
result.add(allVersions.get(listIndex));
} else {
result.add(allVersions.get(allVersions.size() - 1));
}
}
} else if (version.isVersionAction()) {
final Action versionAction = version.getVersionAction();
List<Ref> allInAscendingOrder = getAllInAscendingOrder(ggit,
featureCommits, featureId);
switch (versionAction) {
case ALL:
result.addAll(allInAscendingOrder);
break;
case FIRST:
if (allInAscendingOrder.size() > 0) {
result.add(allInAscendingOrder.get(0));
}
break;
case LAST:
if (allInAscendingOrder.size() > 0) {
result.add(allInAscendingOrder.get(allInAscendingOrder
.size() - 1));
}
break;
case NEXT:
Ref next = next(requestedVersionRef, allInAscendingOrder);
if (next != null) {
result.add(next);
}
break;
case PREVIOUS:
Ref previous = previous(requestedVersionRef,
allInAscendingOrder);
if (previous != null) {
result.add(previous);
}
break;
default:
break;
}
}
}
return result.iterator();
}
private RevCommit findClosest(final Date date,
Iterator<RevCommit> commitsInDescendingOrder) {
final long requestedTime = date.getTime();
RevCommit closest = null;
while (commitsInDescendingOrder.hasNext()) {
RevCommit current = commitsInDescendingOrder.next();
if (closest == null) {
closest = current;
} else {
long delta = Math.abs(current.getTimestamp() - requestedTime);
long prevDelta = Math.abs(closest.getTimestamp()
- requestedTime);
if (delta < prevDelta) {
closest = current;
}
}
}
return closest;
}
private long toSecondsPrecision(final long timeStampMillis) {
return timeStampMillis / 1000;
}
private Ref previous(Ref requestedVersionRef, List<Ref> allVersions) {
int idx = locate(requestedVersionRef, allVersions);
if (idx > 0) {
return allVersions.get(idx - 1);
}
return null;
}
private Ref next(Ref requestedVersionRef, List<Ref> allVersions) {
int idx = locate(requestedVersionRef, allVersions);
if (idx > -1 && idx < allVersions.size() - 1) {
return allVersions.get(idx + 1);
}
return null;
}
private int locate(final Ref requestedVersionRef, List<Ref> allVersions) {
if (requestedVersionRef == null) {
return -1;
}
for (int i = 0; i < allVersions.size(); i++) {
Ref ref = allVersions.get(i);
if (requestedVersionRef.equals(ref)) {
return i;
}
}
return -1;
}
private List<Ref> getAllInAscendingOrder(final GeoGIT ggit,
final Iterator<RevCommit> commits, final String featureId)
throws Exception {
LinkedList<Ref> featureRefs = new LinkedList<Ref>();
final List<String> path = path(featureId);
// find all commits where this feature is touched
while (commits.hasNext()) {
RevCommit commit = commits.next();
ObjectId commitId = commit.getId();
ObjectId parentCommitId = commit.getParentIds().get(0);
DiffOp diffOp = ggit.diff().setOldVersion(parentCommitId)
.setNewVersion(commitId).setFilter(path);
Iterator<DiffEntry> diffs = diffOp.call();
Preconditions.checkState(diffs.hasNext());
DiffEntry diff = diffs.next();
Preconditions.checkState(!diffs.hasNext());
switch (diff.getType()) {
case ADD:
case MODIFY:
featureRefs.addFirst(diff.getNewObject());
break;
case DELETE:
break;
}
}
return featureRefs;
}
/**
* Extracts the feature version from the given {@code rid} if supplied, or
* finds out the current feature version from the feature id otherwise.
*
* @return the version identifier of the feature given by {@code version},
* or at the current geogit HEAD if {@code version == null}, or
* {@code null} if such a feature does not exist.
*/
private Ref extractRequestedVersion(final GeoGIT ggit,
final String featureId, final String version) {
final Repository repository = ggit.getRepository();
if (version != null) {
ObjectId versionedId = ObjectId.valueOf(version);
// verify the object exists
StagingDatabase stagingDatabase = repository.getIndex()
.getDatabase();
boolean exists = stagingDatabase.exists(versionedId);
// Ref rootTreeChild = repository.getRootTreeChild(path(featureId));
if (exists) {
return new Ref(featureId, versionedId, TYPE.BLOB);
}
return null;
}
// no version specified, find out the latest
List<String> path = path(featureId);
Ref currFeatureRef = repository.getRootTreeChild(path);
if (currFeatureRef == null) {
// feature does not exist at the current repository state
return null;
}
return currFeatureRef;
}
private List<String> typeNamePath() {
List<String> path = new ArrayList<String>(3);
if (null != typeName.getNamespaceURI()) {
path.add(typeName.getNamespaceURI());
}
path.add(typeName.getLocalPart());
return path;
}
private List<String> path(final String featureId) {
List<String> path = typeNamePath();
path.add(featureId);
return path;
}
}