001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.connector.git; 017 018import java.io.File; 019import java.io.IOException; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import javax.jcr.NamespaceRegistry; 029import javax.jcr.RepositoryException; 030import org.eclipse.jgit.api.Git; 031import org.eclipse.jgit.lib.ObjectId; 032import org.eclipse.jgit.lib.ObjectLoader; 033import org.eclipse.jgit.lib.Repository; 034import org.eclipse.jgit.storage.file.FileRepositoryBuilder; 035import org.modeshape.schematic.document.Document; 036import org.modeshape.jcr.RepositoryConfiguration; 037import org.modeshape.jcr.api.nodetype.NodeTypeManager; 038import org.modeshape.jcr.cache.DocumentStoreException; 039import org.modeshape.jcr.spi.federation.Connector; 040import org.modeshape.jcr.spi.federation.DocumentWriter; 041import org.modeshape.jcr.spi.federation.PageKey; 042import org.modeshape.jcr.spi.federation.PageWriter; 043import org.modeshape.jcr.spi.federation.Pageable; 044import org.modeshape.jcr.spi.federation.ReadOnlyConnector; 045import org.modeshape.jcr.value.binary.ExternalBinaryValue; 046 047/** 048 * A read-only {@link Connector} that accesses the content in a local Git repository that is a clone of a remote repository. 049 * <p> 050 * This connector has several properties that must be configured via the {@link RepositoryConfiguration}: 051 * <ul> 052 * <li><strong><code>directoryPath</code></strong> - The path to the folder that is or contains the <code>.git</code> data 053 * structure is to be accessed by this connector.</li> 054 * <li><strong><code>remoteName</code></strong> - The alias used by the local Git repository for the remote repository. The 055 * default is the "<code>origin</code>". If the value contains commas, the value contains an ordered list of remote aliases that 056 * should be searched; the first one to match an existing remote will be used.</li> 057 * <li><strong><code>queryableBranches</code></strong> - An array with the names of the branches that should be queryable by the 058 * repository. By default, only the master branch is queryable.</li> 059 * </ul> 060 * </p> 061 * <p> 062 * The connector results in the following structure: 063 * </p> 064 * <table cellspacing="0" cellpadding="1" border="1"> 065 * <tr> 066 * <th>Path</th> 067 * <th>Description</th> 068 * </tr> 069 * <tr> 070 * <td><code>/branches/{branchName}</code></td> 071 * <td>The list of branches.</td> 072 * </tr> 073 * <tr> 074 * <td><code>/tags/{tagName}</code></td> 075 * <td>The list of tags.</td> 076 * </tr> 077 * <tr> 078 * <td><code>/commits/{branchOrTagNameOrCommit}/{objectId}</code></td> 079 * <td>The history of commits on the branch, tag or object ID name "<code>{branchOrTagNameOrCommit}</code>", where " 080 * <code>{objectId}</code>" is the object ID of the commit.</td> 081 * </tr> 082 * <tr> 083 * <td><code>/commit/{branchOrTagNameOrCommit}</code></td> 084 * <td>The information about a particular branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>".</td> 085 * </tr> 086 * <tr> 087 * <td><code>/tree/{branchOrTagOrObjectId}/{filesAndFolders}/...</code></td> 088 * <td>The structure of the directories and files in the specified branch, tag or commit "<code>{branchOrTagNameOrCommit}</code>". 089 * </td> 090 * </tr> 091 * </table> 092 */ 093public class GitConnector extends ReadOnlyConnector implements Pageable { 094 095 private static final boolean DEFAULT_INCLUDE_MIME_TYPE = false; 096 private static final String GIT_DIRECTORY_NAME = ".git"; 097 098 private static final String GIT_CND_PATH = "org/modeshape/connector/git/git.cnd"; 099 100 /** 101 * The string path for a {@link File} object that represents the top-level directory of the local Git repository. This is set 102 * via reflection and is required for this connector. 103 */ 104 private String directoryPath; 105 106 /** 107 * The optional string value representing the name of the remote that serves as the primary remote repository. By default this 108 * is "origin". This is set via reflection. 109 */ 110 private String remoteName; 111 112 /** 113 * The optional string value representing the name of the remote that serves as the primary remote repository. By default this 114 * is "origin". This is set via reflection. 115 */ 116 private List<String> parsedRemoteNames; 117 118 /** 119 * The optional boolean value specifying whether the connector should set the "jcr:mimeType" property on the "jcr:content" 120 * child node under each "git:file" node. By default this is '{@value GitConnector#DEFAULT_INCLUDE_MIME_TYPE}'. This is set 121 * via reflection. 122 */ 123 private boolean includeMimeType = DEFAULT_INCLUDE_MIME_TYPE; 124 125 private Repository repository; 126 private Git git; 127 private Map<String, GitFunction> functions; 128 private Map<String, PageableGitFunction> pageableFunctions; 129 private Values values; 130 131 @Override 132 public void initialize( NamespaceRegistry registry, 133 NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException { 134 super.initialize(registry, nodeTypeManager); 135 136 // Verify the local git repository exists ... 137 File dir = new File(directoryPath); 138 if (!dir.exists() || !dir.isDirectory()) { 139 throw new RepositoryException(GitI18n.directoryDoesNotExist.text(dir.getAbsolutePath())); 140 } 141 if (!dir.canRead()) { 142 throw new RepositoryException(GitI18n.directoryCannotBeRead.text(dir.getAbsolutePath())); 143 } 144 File gitDir = dir; 145 if (!GIT_DIRECTORY_NAME.equals(gitDir.getName())) { 146 gitDir = new File(dir, ".git"); 147 if (!gitDir.exists() || !gitDir.isDirectory()) { 148 throw new RepositoryException(GitI18n.directoryDoesNotExist.text(gitDir.getAbsolutePath())); 149 } 150 if (!gitDir.canRead()) { 151 throw new RepositoryException(GitI18n.directoryCannotBeRead.text(gitDir.getAbsolutePath())); 152 } 153 } 154 155 values = new Values(factories(), getContext().getBinaryStore()); 156 157 // Set up the repository instance. We expect it to exist, and will use it as a "bare" repository (meaning 158 // that no working directory will be used nor needs to exist) ... 159 repository = new FileRepositoryBuilder().setGitDir(gitDir).setMustExist(true).setBare().build(); 160 git = new Git(repository); 161 162 parsedRemoteNames = new ArrayList<String>(); 163 if (this.remoteName != null) { 164 // Make sure the remote exists ... 165 Set<String> remoteNames = repository.getConfig().getSubsections("remote"); 166 String remoteName = null; 167 for (String desiredName : this.remoteName.split(",")) { 168 desiredName = desiredName.trim(); 169 if (remoteNames.contains(desiredName)) { 170 remoteName = desiredName; 171 parsedRemoteNames.add(desiredName); 172 break; 173 } 174 } 175 if (remoteName == null) { 176 throw new RepositoryException(GitI18n.remoteDoesNotExist.text(this.remoteName, gitDir.getAbsolutePath())); 177 } 178 this.remoteName = remoteName; 179 } 180 181 // Register the different functions ... 182 functions = new HashMap<String, GitFunction>(); 183 pageableFunctions = new HashMap<String, PageableGitFunction>(); 184 register(new GitRoot(this), new GitBranches(this), new GitTags(this), new GitHistory(this), new GitCommitDetails(this), 185 new GitTree(this)); 186 187 // Register the Git-specific node types ... 188 InputStream cndStream = getClass().getClassLoader().getResourceAsStream(GIT_CND_PATH); 189 nodeTypeManager.registerNodeTypes(cndStream, true); 190 191 } 192 193 private void register( GitFunction... functions ) { 194 for (GitFunction function : functions) { 195 this.functions.put(function.getName(), function); 196 if (function instanceof PageableGitFunction) { 197 this.pageableFunctions.put(function.getName(), (PageableGitFunction)function); 198 } 199 } 200 } 201 202 protected DocumentWriter newDocumentWriter( String id ) { 203 return super.newDocument(id); 204 } 205 206 protected boolean includeMimeType() { 207 return includeMimeType; 208 } 209 210 @Override 211 public void shutdown() { 212 repository = null; 213 git = null; 214 functions = null; 215 } 216 217 @Override 218 public Document getDocumentById( String id ) { 219 CallSpecification callSpec = new CallSpecification(id); 220 GitFunction function = functions.get(callSpec.getFunctionName()); 221 if (function == null) return null; 222 try { 223 // Set up the document writer ... 224 DocumentWriter writer = newDocument(id); 225 String parentId = callSpec.getParentId(); 226 assert parentId != null; 227 writer.setParent(parentId); 228 // check if the document should be indexed or not, based on the global connector setting and the specific function 229 if (!this.isQueryable() || !function.isQueryable(callSpec)) { 230 writer.setNotQueryable(); 231 } 232 // Now call the function ... 233 Document doc = function.execute(repository, git, callSpec, writer, values); 234 // Log the result ... 235 getLogger().trace("ID={0},result={1}", id, doc); 236 return doc; 237 } catch (Throwable e) { 238 throw new DocumentStoreException(id, e); 239 } 240 } 241 242 @Override 243 public Document getChildren( PageKey pageKey ) { 244 String id = pageKey.getParentId(); 245 CallSpecification callSpec = new CallSpecification(id); 246 PageableGitFunction function = pageableFunctions.get(callSpec.getFunctionName()); 247 if (function == null) return null; 248 try { 249 // Set up the document writer ... 250 PageWriter writer = newPageDocument(pageKey); 251 // Now call the function ... 252 return function.execute(repository, git, callSpec, writer, values, pageKey); 253 } catch (Throwable e) { 254 throw new DocumentStoreException(id, e); 255 } 256 } 257 258 @Override 259 public Document getChildReference( String parentKey, 260 String childKey ) { 261 // The child key always contains the path to the child, so therefore we can always use it to create the 262 // child reference document ... 263 CallSpecification callSpec = new CallSpecification(childKey); 264 return newChildReference(childKey, callSpec.lastParameter()); 265 } 266 267 @Override 268 public String getDocumentId( String path ) { 269 // Our paths are basically used as IDs ... 270 return path; 271 } 272 273 @Override 274 public Collection<String> getDocumentPathsById( String id ) { 275 // Our paths are basically used as IDs, so the ID is the path ... 276 return Collections.singletonList(id); 277 } 278 279 @Override 280 public boolean hasDocument( String id ) { 281 Document doc = getDocumentById(id); 282 return doc != null; 283 } 284 285 @Override 286 public ExternalBinaryValue getBinaryValue( String id ) { 287 try { 288 ObjectId fileObjectId = ObjectId.fromString(id); 289 ObjectLoader fileLoader = repository.open(fileObjectId); 290 return new GitBinaryValue(fileObjectId, fileLoader, getSourceName(), null, getMimeTypeDetector()); 291 } catch (IOException e) { 292 throw new DocumentStoreException(id, e); 293 } 294 } 295 296 protected final String remoteName() { 297 return remoteName; 298 } 299 300 protected final List<String> remoteNames() { 301 return parsedRemoteNames; 302 } 303}