/* * This is a common dao with basic CRUD operations and is not limited to any * persistent layer implementation * * Copyright (C) 2008 Imran M Yousuf (imyousuf@smartitengineering.com) * * 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; either * version 3 of the License, or (at your option) any later version. * 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. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package com.smartitengineering.version.impl.jgit; import com.smartitengineering.dao.common.queryparam.QueryParameter; import com.smartitengineering.version.api.Commit; import com.smartitengineering.version.api.Resource; import com.smartitengineering.version.api.Revision; import com.smartitengineering.version.api.VersionedResource; import com.smartitengineering.version.api.dao.VersionControlReadDao; import com.smartitengineering.version.api.dao.VersionControlWriteDao; import com.smartitengineering.version.api.dao.WriteStatus; import com.smartitengineering.version.api.dao.WriterCallback; import com.smartitengineering.version.api.factory.VersionAPI; import com.smartitengineering.version.api.spi.MutableCommit; import com.smartitengineering.version.api.spi.MutableRevision; import com.smartitengineering.version.impl.jgit.service.MetaFactory; import com.smartitengineering.version.impl.jgit.service.RCSConfig; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.lang.StringUtils; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileTreeEntry; import org.eclipse.jgit.lib.GitIndex; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectWriter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Tree; import org.eclipse.jgit.lib.TreeEntry; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; /** * * @author imyousuf */ public class JGitImpl implements VersionControlWriteDao, VersionControlReadDao, JGitDaoExtension { private String readRepositoryLocation; private String writeRepositoryLocation; private Repository writeRepository; private ObjectWriter objectWriter; private Repository readRepository; private ExecutorService executorService; private RCSConfig config; private boolean initialized; public JGitImpl() { } public void init() throws IOException { if (initialized) { throw new IllegalStateException("Impl already initialized"); } if (StringUtils.isBlank(getReadRepositoryLocation()) || StringUtils.isBlank(getWriteRepositoryLocation())) { throw new IllegalStateException("Repository location not set!"); } File writeRepoDir = new File(getWriteRepositoryLocation()); writeRepository = new Repository(writeRepoDir); if (!writeRepoDir.exists()) { writeRepository.create(); } writeRepository.close(); File readRepoDir = new File(getReadRepositoryLocation()); readRepository = new Repository(readRepoDir); if (!readRepoDir.exists()) { readRepository.create(); } readRepository.close(); executorService = Executors.newFixedThreadPool(getConfig(). getConcurrentWriteOperations()); initialized = true; } public void finish() { if (writeRepository != null) { writeRepository.close(); } if (readRepository != null) { readRepository.close(); } } protected void checkInitialized() { if (!initialized) { throw new IllegalArgumentException("After constructing please " + "set repository location and then invoke init() before " + "attempting to use any other operations"); } } public RCSConfig getConfig() { if (config == null) { return MetaFactory.getInstance().getConfig(); } return config; } public void setConfig(RCSConfig config) { this.config = config; } public void setRepositoryLocation(final String repositoryLocation) { if (StringUtils.isBlank(repositoryLocation)) { throw new IllegalArgumentException("Repo location can't be blank!"); } this.readRepositoryLocation = repositoryLocation; this.writeRepositoryLocation = repositoryLocation; } public String getReadRepositoryLocation() { if (StringUtils.isBlank(readRepositoryLocation)) { return getConfig().getRepositoryReadPath(); } return readRepositoryLocation; } public void setReadRepositoryLocation(final String readRepositoryLocation) { if (StringUtils.isBlank(readRepositoryLocation)) { throw new IllegalArgumentException("Repo location can't be blank!"); } this.readRepositoryLocation = readRepositoryLocation; } public String getWriteRepositoryLocation() { if (StringUtils.isBlank(writeRepositoryLocation)) { return getConfig().getRepositoryWritePath(); } return writeRepositoryLocation; } public void setWriteRepositoryLocation(final String writeRepositoryLocation) { if (StringUtils.isBlank(writeRepositoryLocation)) { throw new IllegalArgumentException("Repo location can't be blank!"); } this.writeRepositoryLocation = writeRepositoryLocation; } public synchronized Repository getReadRepository() { return readRepository; } protected synchronized void reInitReadRepository() throws IOException { readRepository.close(); readRepository = new Repository(new File(getReadRepositoryLocation())); } public Repository getWriteRepository() { return writeRepository; } public void store(final Commit commit, final WriterCallback callback) { executorService.submit(new Runnable() { public void run() { WriteStatus status = null; String comment = null; Throwable error = null; try { Tree head = getHeadTree(writeRepository); addOrUpdateToHead(commit, head); prepareCommit(head, commit); status = WriteStatus.STORE_PASS; comment = "OK"; error = null; } catch (Throwable ex) { status = WriteStatus.STORE_FAIL; comment = ex.getMessage(); error = ex; throw new RuntimeException(ex); } finally { if (callback != null) { callback.handle(commit, status, comment, error); } try { reInitReadRepository(); } catch (IOException ex) { ex.printStackTrace(); } } } }); } public VersionedResource getVersionedResource(final String resourceId) { try { String trimmedResourceId = VersionAPI.trimToProperResourceId( resourceId); if (StringUtils.isBlank(trimmedResourceId)) { throw new IllegalArgumentException("Invalid resource id!"); } Set<ObjectId> revisionIds = getGraphForResourceId(trimmedResourceId); if (revisionIds == null || revisionIds.isEmpty()) { throw new IllegalArgumentException("Resource id doesn't exist!"); } Revision[] revisions = new Revision[revisionIds.size()]; int i = 0; for (ObjectId revisionId : revisionIds) { String revisionIdStr = ObjectId.toString(revisionId); String content = new String(readObject(revisionIdStr)); revisions[i++] = VersionAPI.createRevision(VersionAPI.createResource(trimmedResourceId, content), revisionIdStr); } return VersionAPI.createVersionedResource(Arrays.asList(revisions)); } catch (Throwable ex) { throw new RuntimeException(ex); } } public Resource getResource(final String resourceId) { try { String trimmedResourceId = VersionAPI.trimToProperResourceId( resourceId); if (StringUtils.isBlank(trimmedResourceId)) { throw new IllegalArgumentException("Invalid resource id!"); } ObjectId resourceObjectId; Tree head = getHeadTree(getReadRepository()); if (!head.existsBlob(trimmedResourceId)) { throw new IllegalArgumentException("Resource id doesn't exist!"); } TreeEntry treeEntry = head.findBlobMember(trimmedResourceId); resourceObjectId = treeEntry.getId(); return VersionAPI.createResource(trimmedResourceId, new String( readObject(ObjectId.toString(resourceObjectId)))); } catch (Throwable ex) { throw new RuntimeException(ex); } } public Resource getResourceByRevision(final String revisionId, final String resourceId) { try { String trimmedResourceId = VersionAPI.trimToProperResourceId( resourceId); if (StringUtils.isBlank(trimmedResourceId)) { throw new IllegalArgumentException("Invalid resource id!"); } ObjectId resourceObjectId; resourceObjectId = ObjectId.fromString(revisionId); return VersionAPI.createResource(trimmedResourceId, new String( readObject(ObjectId.toString(resourceObjectId)))); } catch (Throwable ex) { throw new RuntimeException(ex); } } public byte[] readObject(final String objectIdStr) throws IOException, IllegalArgumentException { if (StringUtils.isBlank(objectIdStr)) { throw new IllegalArgumentException("Invalid Object id!"); } ObjectId objectId = ObjectId.fromString(objectIdStr); ObjectLoader objectLoader = getReadRepository().openObject(objectId); if (objectLoader.getType() != Constants.OBJ_BLOB) { throw new IllegalArgumentException("Not a blob: " + objectIdStr); } return objectLoader.getBytes(); } public Collection<Commit> searchForCommits( final Collection<QueryParameter> parameters) { throw new UnsupportedOperationException("Not supported yet."); } public Collection<Revision> searchForRevisions( final Collection<QueryParameter> parameters) { throw new UnsupportedOperationException("Not supported yet."); } public Map<String, byte[]> readBlobObjects(final String... objectIds) throws IOException, IllegalArgumentException { checkInitialized(); if (objectIds == null || objectIds.length <= 0) { throw new IllegalArgumentException("Empty Object IDs!"); } Map<String, byte[]> blobs = new HashMap<String, byte[]>(objectIds.length); for (String objectIdStr : objectIds) { byte[] bytes = readObject(objectIdStr); blobs.put(objectIdStr, bytes); } return blobs; } protected Tree getHeadTree(Repository repository) throws IOException { Tree head; org.eclipse.jgit.lib.Commit headCommit = repository.mapCommit( Constants.HEAD); if (headCommit == null) { head = new Tree(repository); } else { head = headCommit.getTree(); } return head; } protected ObjectId addOrUpdateToHead(final Commit commit, final Tree head) throws IOException { for (Revision revision : commit.getRevisions()) { String objectPath = revision.getResource().getId(); FileTreeEntry treeEntry; boolean newEntry = false; if (head.existsBlob(objectPath)) { treeEntry = (FileTreeEntry) head.findBlobMember(objectPath); } else { treeEntry = head.addFile(objectPath); newEntry = true; } treeEntry.setExecutable(false); if (!revision.getResource().isDeleted()) { if (revision instanceof MutableRevision) { ObjectId revisionId = getObjectWriter().writeBlob( revision.getResource().getContentSize(), revision.getResource().getContentAsStream()); MutableRevision mutableRevision = (MutableRevision) revision; mutableRevision.setRevisionId(ObjectId.toString(revisionId)); treeEntry.setId(revisionId); } else { throw new IllegalArgumentException( "SPI not implemented by API!"); } } else if (!newEntry) { if (revision instanceof MutableRevision) { ObjectId revisionId = treeEntry.getId(); MutableRevision mutableRevision = (MutableRevision) revision; mutableRevision.setRevisionId(ObjectId.toString(revisionId)); treeEntry.delete(); } else { throw new IllegalArgumentException( "SPI not implemented by API!"); } } } GitIndex index = getWriteRepository().getIndex(); index.readTree(head); index.write(); ObjectId newHeadId = index.writeTree(); head.setId(newHeadId); return newHeadId; } protected ObjectWriter getObjectWriter() { if (objectWriter == null) { objectWriter = new ObjectWriter(getWriteRepository()); } return objectWriter; } protected void performCommit(final Commit newCommit, final Tree head) throws IOException { ObjectId[] parentIds; ObjectId currentHeadId = getWriteRepository().resolve(Constants.HEAD); if (currentHeadId != null) { parentIds = new ObjectId[]{currentHeadId}; } else { parentIds = new ObjectId[0]; } org.eclipse.jgit.lib.Commit commit = new org.eclipse.jgit.lib.Commit( getWriteRepository(), parentIds); commit.setTree(head); commit.setTreeId(head.getId()); PersonIdent person = new PersonIdent(newCommit.getAuthor().getName(), newCommit.getAuthor().getEmail()); commit.setAuthor(person); commit.setCommitter(person); commit.setMessage(newCommit.getCommitMessage()); ObjectId newCommitId = getObjectWriter().writeCommit(commit); if (newCommit instanceof MutableCommit) { MutableCommit mutableCommit = (MutableCommit) newCommit; mutableCommit.setCommitId(ObjectId.toString(newCommitId)); mutableCommit.setCommitTime(commit.getCommitter().getWhen()); commit.setCommitId(newCommitId); if (commit.getParentIds().length > 0) { mutableCommit.setParentCommitId(ObjectId.toString(commit.getParentIds()[0])); } else { mutableCommit.setParentCommitId(ObjectId.toString(ObjectId.zeroId())); } } else { throw new IllegalArgumentException("SPI not implemented by API!"); } RefUpdate refUpdate = getWriteRepository().updateRef(Constants.HEAD); refUpdate.setNewObjectId(commit.getCommitId()); refUpdate.setRefLogMessage(commit.getMessage(), false); refUpdate.forceUpdate(); } protected void prepareCommit(final Tree head, final Commit commit) throws IOException, IOException { ObjectId currentHeadId = getWriteRepository().resolve(Constants.HEAD); boolean commitAvailable = true; if (currentHeadId != null) { org.eclipse.jgit.lib.Commit headCommit = writeRepository.mapCommit(currentHeadId); if (headCommit != null) { Tree headTree = headCommit.getTree(); if (headTree != null && head.getId().equals(headTree.getId())) { commitAvailable = false; } } } if (commitAvailable || getConfig().isAllowNoChangeCommit()) { performCommit(commit, head); } } protected Set<ObjectId> getGraphForResourceId(String resourceId) throws MissingObjectException, IncorrectObjectTypeException, IOException { final Set<ObjectId> versions = new LinkedHashSet<ObjectId>(); final RevWalk rw = new RevWalk(getReadRepository()); final TreeWalk tw = new TreeWalk(getReadRepository()); rw.markStart(rw.parseCommit(getReadRepository().resolve(Constants.HEAD))); tw.setFilter(TreeFilter.ANY_DIFF); RevCommit c; while ((c = rw.next()) != null) { final ObjectId[] p = new ObjectId[c.getParentCount() + 1]; for (int i = 0; i < c.getParentCount(); i++) { rw.parseAny(c.getParent(i)); p[i] = c.getParent(i).getTree(); } final int me = p.length - 1; p[me] = c.getTree(); tw.reset(p); while (tw.next()) { if (tw.getFileMode(me).getObjectType() == Constants.OBJ_BLOB) { // This path was modified relative to the ancestor(s) String s = tw.getPathString(); if (s != null && s.equals(resourceId)) { versions.add(tw.getObjectId(me)); } } if (tw.isSubtree()) { // make sure we recurse into modified directories tw.enterSubtree(); } } } return versions; } }