/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.connector.git;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.modeshape.schematic.document.Document;
import org.modeshape.jcr.RepositoryConfiguration;
import org.modeshape.jcr.api.nodetype.NodeTypeManager;
import org.modeshape.jcr.cache.DocumentStoreException;
import org.modeshape.jcr.spi.federation.Connector;
import org.modeshape.jcr.spi.federation.DocumentWriter;
import org.modeshape.jcr.spi.federation.PageKey;
import org.modeshape.jcr.spi.federation.PageWriter;
import org.modeshape.jcr.spi.federation.Pageable;
import org.modeshape.jcr.spi.federation.ReadOnlyConnector;
import org.modeshape.jcr.value.binary.ExternalBinaryValue;
/**
* A read-only {@link Connector} that accesses the content in a local Git repository that is a clone of a remote repository.
* <p>
* This connector has several properties that must be configured via the {@link RepositoryConfiguration}:
* <ul>
* <li><strong><code>directoryPath</code></strong> - The path to the folder that is or contains the <code>.git</code> data
* structure is to be accessed by this connector.</li>
* <li><strong><code>remoteName</code></strong> - The alias used by the local Git repository for the remote repository. The
* default is the "<code>origin</code>". If the value contains commas, the value contains an ordered list of remote aliases that
* should be searched; the first one to match an existing remote will be used.</li>
* <li><strong><code>queryableBranches</code></strong> - An array with the names of the branches that should be queryable by the
* repository. By default, only the master branch is queryable.</li>
* </ul>
* </p>
* <p>
* The connector results in the following structure:
* </p>
* <table cellspacing="0" cellpadding="1" border="1">
* <tr>
* <th>Path</th>
* <th>Description</th>
* </tr>
* <tr>
* <td><code>/branches/{branchName}</code></td>
* <td>The list of branches.</td>
* </tr>
* <tr>
* <td><code>/tags/{tagName}</code></td>
* <td>The list of tags.</td>
* </tr>
* <tr>
* <td><code>/commits/{branchOrTagNameOrCommit}/{objectId}</code></td>
* <td>The history of commits on the branch, tag or object ID name "<code>{branchOrTagNameOrCommit}</code>", where "
* <code>{objectId}</code>" is the object ID of the commit.</td>
* </tr>
* <tr>
* <td><code>/commit/{branchOrTagNameOrCommit}</code></td>
* <td>The information about a particular branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>".</td>
* </tr>
* <tr>
* <td><code>/tree/{branchOrTagOrObjectId}/{filesAndFolders}/...</code></td>
* <td>The structure of the directories and files in the specified branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>".
* </td>
* </tr>
* </table>
*/
public class GitConnector extends ReadOnlyConnector implements Pageable {
private static final boolean DEFAULT_INCLUDE_MIME_TYPE = false;
private static final String GIT_DIRECTORY_NAME = ".git";
private static final String GIT_CND_PATH = "org/modeshape/connector/git/git.cnd";
/**
* The string path for a {@link File} object that represents the top-level directory of the local Git repository. This is set
* via reflection and is required for this connector.
*/
private String directoryPath;
/**
* The optional string value representing the name of the remote that serves as the primary remote repository. By default this
* is "origin". This is set via reflection.
*/
private String remoteName;
/**
* The optional string value representing the name of the remote that serves as the primary remote repository. By default this
* is "origin". This is set via reflection.
*/
private List<String> parsedRemoteNames;
/**
* The optional boolean value specifying whether the connector should set the "jcr:mimeType" property on the "jcr:content"
* child node under each "git:file" node. By default this is '{@value GitConnector#DEFAULT_INCLUDE_MIME_TYPE}'. This is set
* via reflection.
*/
private boolean includeMimeType = DEFAULT_INCLUDE_MIME_TYPE;
private Repository repository;
private Git git;
private Map<String, GitFunction> functions;
private Map<String, PageableGitFunction> pageableFunctions;
private Values values;
@Override
public void initialize( NamespaceRegistry registry,
NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException {
super.initialize(registry, nodeTypeManager);
// Verify the local git repository exists ...
File dir = new File(directoryPath);
if (!dir.exists() || !dir.isDirectory()) {
throw new RepositoryException(GitI18n.directoryDoesNotExist.text(dir.getAbsolutePath()));
}
if (!dir.canRead()) {
throw new RepositoryException(GitI18n.directoryCannotBeRead.text(dir.getAbsolutePath()));
}
File gitDir = dir;
if (!GIT_DIRECTORY_NAME.equals(gitDir.getName())) {
gitDir = new File(dir, ".git");
if (!gitDir.exists() || !gitDir.isDirectory()) {
throw new RepositoryException(GitI18n.directoryDoesNotExist.text(gitDir.getAbsolutePath()));
}
if (!gitDir.canRead()) {
throw new RepositoryException(GitI18n.directoryCannotBeRead.text(gitDir.getAbsolutePath()));
}
}
values = new Values(factories(), getContext().getBinaryStore());
// Set up the repository instance. We expect it to exist, and will use it as a "bare" repository (meaning
// that no working directory will be used nor needs to exist) ...
repository = new FileRepositoryBuilder().setGitDir(gitDir).setMustExist(true).setBare().build();
git = new Git(repository);
parsedRemoteNames = new ArrayList<String>();
if (this.remoteName != null) {
// Make sure the remote exists ...
Set<String> remoteNames = repository.getConfig().getSubsections("remote");
String remoteName = null;
for (String desiredName : this.remoteName.split(",")) {
desiredName = desiredName.trim();
if (remoteNames.contains(desiredName)) {
remoteName = desiredName;
parsedRemoteNames.add(desiredName);
break;
}
}
if (remoteName == null) {
throw new RepositoryException(GitI18n.remoteDoesNotExist.text(this.remoteName, gitDir.getAbsolutePath()));
}
this.remoteName = remoteName;
}
// Register the different functions ...
functions = new HashMap<String, GitFunction>();
pageableFunctions = new HashMap<String, PageableGitFunction>();
register(new GitRoot(this), new GitBranches(this), new GitTags(this), new GitHistory(this), new GitCommitDetails(this),
new GitTree(this));
// Register the Git-specific node types ...
InputStream cndStream = getClass().getClassLoader().getResourceAsStream(GIT_CND_PATH);
nodeTypeManager.registerNodeTypes(cndStream, true);
}
private void register( GitFunction... functions ) {
for (GitFunction function : functions) {
this.functions.put(function.getName(), function);
if (function instanceof PageableGitFunction) {
this.pageableFunctions.put(function.getName(), (PageableGitFunction)function);
}
}
}
protected DocumentWriter newDocumentWriter( String id ) {
return super.newDocument(id);
}
protected boolean includeMimeType() {
return includeMimeType;
}
@Override
public void shutdown() {
repository = null;
git = null;
functions = null;
}
@Override
public Document getDocumentById( String id ) {
CallSpecification callSpec = new CallSpecification(id);
GitFunction function = functions.get(callSpec.getFunctionName());
if (function == null) return null;
try {
// Set up the document writer ...
DocumentWriter writer = newDocument(id);
String parentId = callSpec.getParentId();
assert parentId != null;
writer.setParent(parentId);
// check if the document should be indexed or not, based on the global connector setting and the specific function
if (!this.isQueryable() || !function.isQueryable(callSpec)) {
writer.setNotQueryable();
}
// Now call the function ...
Document doc = function.execute(repository, git, callSpec, writer, values);
// Log the result ...
getLogger().trace("ID={0},result={1}", id, doc);
return doc;
} catch (Throwable e) {
throw new DocumentStoreException(id, e);
}
}
@Override
public Document getChildren( PageKey pageKey ) {
String id = pageKey.getParentId();
CallSpecification callSpec = new CallSpecification(id);
PageableGitFunction function = pageableFunctions.get(callSpec.getFunctionName());
if (function == null) return null;
try {
// Set up the document writer ...
PageWriter writer = newPageDocument(pageKey);
// Now call the function ...
return function.execute(repository, git, callSpec, writer, values, pageKey);
} catch (Throwable e) {
throw new DocumentStoreException(id, e);
}
}
@Override
public Document getChildReference( String parentKey,
String childKey ) {
// The child key always contains the path to the child, so therefore we can always use it to create the
// child reference document ...
CallSpecification callSpec = new CallSpecification(childKey);
return newChildReference(childKey, callSpec.lastParameter());
}
@Override
public String getDocumentId( String path ) {
// Our paths are basically used as IDs ...
return path;
}
@Override
public Collection<String> getDocumentPathsById( String id ) {
// Our paths are basically used as IDs, so the ID is the path ...
return Collections.singletonList(id);
}
@Override
public boolean hasDocument( String id ) {
Document doc = getDocumentById(id);
return doc != null;
}
@Override
public ExternalBinaryValue getBinaryValue( String id ) {
try {
ObjectId fileObjectId = ObjectId.fromString(id);
ObjectLoader fileLoader = repository.open(fileObjectId);
return new GitBinaryValue(fileObjectId, fileLoader, getSourceName(), null, getMimeTypeDetector());
} catch (IOException e) {
throw new DocumentStoreException(id, e);
}
}
protected final String remoteName() {
return remoteName;
}
protected final List<String> remoteNames() {
return parsedRemoteNames;
}
}