/* 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:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.web.api.commands;
import java.io.Writer;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.geotools.util.Range;
import org.locationtech.geogig.api.Context;
import org.locationtech.geogig.api.GeoGIG;
import org.locationtech.geogig.api.NodeRef;
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.plumbing.FindTreeChild;
import org.locationtech.geogig.api.plumbing.ParseTimestamp;
import org.locationtech.geogig.api.plumbing.RevObjectParse;
import org.locationtech.geogig.api.plumbing.RevParse;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.porcelain.DiffOp;
import org.locationtech.geogig.api.porcelain.LogOp;
import org.locationtech.geogig.storage.FieldType;
import org.locationtech.geogig.web.api.AbstractWebAPICommand;
import org.locationtech.geogig.web.api.CommandContext;
import org.locationtech.geogig.web.api.CommandResponse;
import org.locationtech.geogig.web.api.CommandSpecException;
import org.locationtech.geogig.web.api.ResponseWriter;
import org.locationtech.geogig.web.api.StreamResponse;
import org.opengis.feature.type.PropertyDescriptor;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterators;
/**
* Interface for the Log operation in GeoGig.
*
* Web interface for {@link LogOp}
*/
public class Log extends AbstractWebAPICommand {
Integer skip;
Integer limit;
String since;
String until;
String sinceTime;
String untilTime;
List<String> paths;
private int page;
private int elementsPerPage;
boolean firstParentOnly;
boolean countChanges = false;
boolean returnRange = false;
boolean summary = false;
/**
* Mutator for the limit variable
*
* @param limit - the number of commits to print
*/
public void setLimit(Integer limit) {
this.limit = limit;
}
/**
* Mutator for the offset variable
*
* @param offset - the offset to start listing at
*/
public void setOffset(Integer offset) {
this.skip = offset;
}
/**
* Mutator for the since variable
*
* @param since - the start place to list commits
*/
public void setSince(String since) {
this.since = since;
}
/**
* Mutator for the until variable
*
* @param until - the end place for listing commits
*/
public void setUntil(String until) {
this.until = until;
}
/**
* Mutator for the sinceTime variable
*
* @param since - the start place to list commits
*/
public void setSinceTime(String since) {
this.sinceTime = since;
}
/**
* Mutator for the untilTime variable
*
* @param until - the end place for listing commits
*/
public void setUntilTime(String until) {
this.untilTime = until;
}
/**
* Mutator for the paths variable
*
* @param paths - list of paths to filter commits by
*/
public void setPaths(List<String> paths) {
this.paths = paths;
}
/**
* Mutator for the page variable
*
* @param page - the page number to build the response
*/
public void setPage(int page) {
this.page = page;
}
/**
* Mutator for the elementsPerPage variable
*
* @param elementsPerPage - the number of elements to display in the response per page
*/
public void setElementsPerPage(int elementsPerPage) {
this.elementsPerPage = elementsPerPage;
}
/**
* Mutator for the firstParentOnly variable
*
* @param firstParentOnly - true to only show the first parent of a commit
*/
public void setFirstParentOnly(boolean firstParentOnly) {
this.firstParentOnly = firstParentOnly;
}
/**
* Mutator for the countChanges variable
*
* @param countChanges - if true, each commit will include a count of each change type compared
* to its first parent
*/
public void setCountChanges(boolean countChanges) {
this.countChanges = countChanges;
}
/**
* Mutator for the summary variable
*
* @param summary - if true, return all changes from each commit
*/
public void setSummary(boolean summary) {
this.summary = summary;
}
/**
* Mutator for the returnRange variable.
*
* @param returnRange - true to only show the first and last commit of the log, as well as a
* count of the commits in the range.
*/
public void setReturnRange(boolean returnRange) {
this.returnRange = returnRange;
}
/**
* Runs the command and builds the appropriate response
*
* @param context - the context to use for this command
*
* @throws IllegalArgumentException
*/
@Override
public void run(final CommandContext context) {
final Context geogig = this.getCommandLocator(context);
LogOp op = geogig.command(LogOp.class).setFirstParentOnly(firstParentOnly);
if (skip != null) {
op.setSkip(skip.intValue());
}
if (limit != null) {
op.setLimit(limit.intValue());
}
if (this.sinceTime != null || this.untilTime != null) {
Date since = new Date(0);
Date until = new Date();
if (this.sinceTime != null) {
since = new Date(geogig.command(ParseTimestamp.class).setString(this.sinceTime)
.call());
}
if (this.untilTime != null) {
until = new Date(geogig.command(ParseTimestamp.class).setString(this.untilTime)
.call());
}
op.setTimeRange(new Range<Date>(Date.class, since, until));
}
if (this.since != null) {
Optional<ObjectId> since;
since = geogig.command(RevParse.class).setRefSpec(this.since).call();
Preconditions.checkArgument(since.isPresent(), "Object not found '%s'", this.since);
op.setSince(since.get());
}
if (this.until != null) {
Optional<ObjectId> until;
until = geogig.command(RevParse.class).setRefSpec(this.until).call();
Preconditions.checkArgument(until.isPresent(), "Object not found '%s'", this.until);
op.setUntil(until.get());
}
if (paths != null && !paths.isEmpty()) {
for (String path : paths) {
op.addPath(path);
}
}
final Iterator<RevCommit> log = op.call();
Iterators.advance(log, page * elementsPerPage);
if (countChanges) {
final String pathFilter;
if (paths != null && !paths.isEmpty()) {
pathFilter = paths.get(0);
} else {
pathFilter = null;
}
Function<RevCommit, CommitWithChangeCounts> changeCountFunctor = new Function<RevCommit, CommitWithChangeCounts>() {
@Override
public CommitWithChangeCounts apply(RevCommit input) {
ObjectId parent = ObjectId.NULL;
if (input.getParentIds().size() > 0) {
parent = input.getParentIds().get(0);
}
int added = 0;
int modified = 0;
int removed = 0;
// If it's a shallow clone, the commit may not exist
if (parent.equals(ObjectId.NULL) || geogig.stagingDatabase().exists(parent)) {
final Iterator<DiffEntry> diff = geogig.command(DiffOp.class)
.setOldVersion(parent).setNewVersion(input.getId())
.setFilter(pathFilter).call();
while (diff.hasNext()) {
DiffEntry entry = diff.next();
if (entry.changeType() == DiffEntry.ChangeType.ADDED) {
added++;
} else if (entry.changeType() == DiffEntry.ChangeType.MODIFIED) {
modified++;
} else {
removed++;
}
}
}
return new CommitWithChangeCounts(input, added, modified, removed);
}
};
final Iterator<CommitWithChangeCounts> summarizedLog = Iterators.transform(log,
changeCountFunctor);
context.setResponseContent(new CommandResponse() {
@Override
public void write(ResponseWriter out) throws Exception {
out.start();
out.writeCommitsWithChangeCounts(summarizedLog, elementsPerPage);
out.finish();
}
});
} else if (summary) {
if (paths != null && paths.size() > 0) {
context.setResponseContent(new StreamResponse() {
@Override
public void write(Writer out) throws Exception {
writeCSV(context.getGeoGIG(), out, log);
}
});
} else {
throw new CommandSpecException(
"You must specify a feature type path when getting a summary.");
}
} else {
final boolean rangeLog = returnRange;
context.setResponseContent(new CommandResponse() {
@Override
public void write(ResponseWriter out) throws Exception {
out.start();
out.writeCommits(log, elementsPerPage, rangeLog);
out.finish();
}
});
}
}
private void writeCSV(GeoGIG geogig, Writer out, Iterator<RevCommit> log) throws Exception {
String response = "ChangeType,FeatureId,CommitId,Parent CommitIds,Author Name,Author Email,Author Commit Time,Committer Name,Committer Email,Committer Commit Time,Commit Message";
out.write(response);
response = "";
String path = paths.get(0);
// This is the feature type object
Optional<NodeRef> ref = geogig.command(FindTreeChild.class).setChildPath(path)
.setParent(geogig.getRepository().workingTree().getTree()).call();
Optional<RevObject> type = Optional.absent();
if (ref.isPresent()) {
type = geogig.command(RevObjectParse.class)
.setRefSpec(ref.get().getMetadataId().toString()).call();
} else {
throw new CommandSpecException("Couldn't resolve the given path.");
}
if (type.isPresent() && type.get() instanceof RevFeatureType) {
RevFeatureType featureType = (RevFeatureType) type.get();
Collection<PropertyDescriptor> attribs = featureType.type().getDescriptors();
int attributeLength = attribs.size();
for (PropertyDescriptor attrib : attribs) {
response += "," + escapeCsv(attrib.getName().toString());
}
response += '\n';
out.write(response);
response = "";
RevCommit commit = null;
while (log.hasNext()) {
commit = log.next();
String parentId = commit.getParentIds().size() >= 1 ? commit.getParentIds().get(0)
.toString() : ObjectId.NULL.toString();
Iterator<DiffEntry> diff = geogig.command(DiffOp.class).setOldVersion(parentId)
.setNewVersion(commit.getId().toString()).setFilter(path).call();
while (diff.hasNext()) {
DiffEntry entry = diff.next();
response += entry.changeType().toString() + ",";
String fid = "";
if (entry.newPath() != null) {
if (entry.oldPath() != null) {
fid = entry.oldPath() + " -> " + entry.newPath();
} else {
fid = entry.newPath();
}
} else if (entry.oldPath() != null) {
fid = entry.oldPath();
}
response += fid + ",";
response += commit.getId().toString() + ",";
response += parentId;
if (commit.getParentIds().size() > 1) {
for (int index = 1; index < commit.getParentIds().size(); index++) {
response += " " + commit.getParentIds().get(index).toString();
}
}
response += ",";
if (commit.getAuthor().getName().isPresent()) {
response += escapeCsv(commit.getAuthor().getName().get());
}
response += ",";
if (commit.getAuthor().getEmail().isPresent()) {
response += escapeCsv(commit.getAuthor().getEmail().get());
}
response += ","
+ new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z").format(new Date(commit
.getAuthor().getTimestamp())) + ",";
if (commit.getCommitter().getName().isPresent()) {
response += escapeCsv(commit.getCommitter().getName().get());
}
response += ",";
if (commit.getCommitter().getEmail().isPresent()) {
response += escapeCsv(commit.getCommitter().getEmail().get());
}
response += ","
+ new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z").format(new Date(commit
.getCommitter().getTimestamp())) + ",";
String message = escapeCsv(commit.getMessage());
response += message;
if (entry.newObjectId() == ObjectId.NULL) {
// Feature was removed so we need to fill out blank attribute values
for (int index = 0; index < attributeLength; index++) {
response += ",";
}
} else {
// Feature was added or modified so we need to write out the
// attribute
// values from the feature
Optional<RevObject> feature = geogig.command(RevObjectParse.class)
.setObjectId(entry.newObjectId()).call();
RevFeature revFeature = (RevFeature) feature.get();
List<Optional<Object>> values = revFeature.getValues();
for (int index = 0; index < values.size(); index++) {
Optional<Object> value = values.get(index);
PropertyDescriptor attrib = (PropertyDescriptor) attribs.toArray()[index];
String stringValue = "";
if (value.isPresent()) {
FieldType attributeType = FieldType.forBinding(attrib.getType()
.getBinding());
switch (attributeType) {
case DATE:
stringValue = new SimpleDateFormat("MM/dd/yyyy z")
.format((java.sql.Date) value.get());
break;
case DATETIME:
stringValue = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z")
.format((Date) value.get());
break;
case TIME:
stringValue = new SimpleDateFormat("HH:mm:ss z")
.format((Time) value.get());
break;
case TIMESTAMP:
stringValue = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z")
.format((Timestamp) value.get());
break;
default:
stringValue = escapeCsv(value.get().toString());
}
response += "," + stringValue;
} else {
response += ",";
}
}
}
response += '\n';
out.write(response);
response = "";
}
}
} else {
// Couldn't resolve FeatureType
throw new CommandSpecException("Couldn't resolve the given path to a feature type.");
}
}
public class CommitWithChangeCounts {
private final RevCommit commit;
private final int adds;
private final int modifies;
private final int removes;
public CommitWithChangeCounts(RevCommit commit, int adds, int modifies, int removes) {
this.commit = commit;
this.adds = adds;
this.modifies = modifies;
this.removes = removes;
}
public RevCommit getCommit() {
return commit;
}
public int getAdds() {
return adds;
}
public int getModifies() {
return modifies;
}
public int getRemoves() {
return removes;
}
}
private String escapeCsv(String input) {
String returnStr = input.replace("\"", "\"\"");
if (input.contains("\"") || input.contains(",") || input.contains("\n")
|| input.contains("\r")) {
returnStr = "\"" + returnStr + "\"";
}
return returnStr;
}
}