/* * Copyright (c) NASK, NCSC * * This file is part of HoneySpider Network 2.1. * * This is a free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package pl.nask.hsn2.framework.workflow.repository; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.NoMessageException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepository; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.treewalk.TreeWalk; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * * This class is thread safe */ public final class GitWorkflowRepository implements WorkflowRepository { private static class GitFilenameFilter implements FilenameFilter { @Override public boolean accept(File dir, String name) { return !".git".equals(name); } } private static final Logger LOGGER = LoggerFactory .getLogger(GitWorkflowRepository.class); private FileRepository repo; private File repoDir; private final FilenameFilter filenameFilter = new GitFilenameFilter(); public GitWorkflowRepository(String repositoryPath, boolean forceCreate) throws WorkflowRepoException { repoDir = new File(repositoryPath); ensureDirExists(repoDir, forceCreate); ensureDirIsWritable(repoDir); FileRepositoryBuilder builder = new FileRepositoryBuilder(); try { // expecting repositoryPath to be exact, so 'findGitDir()' is not required here repo = builder.setWorkTree(repoDir).findGitDir(repoDir).build(); if (forceCreate && !repo.getConfig().getFile().exists() && checkRepoMightBeCreated(repoDir)) { LOGGER.debug("Workflow repository structure does't exist, trying to create one..."); repo.create(); } if (!repo.getConfig().getFile().exists()) { throw new WorkflowRepoException("Workflow repository structure doesn't exist and cannot be created or is not forced."); } validateGitRepo(); } catch (IOException e) { throw new WorkflowRepoException("Couldn't create repository for path: " + repoDir.getAbsolutePath(), e); } } /** * Checks if repository directory is writable. * * @param directory * Directory file to be checked. * @throws WorkflowRepoException * If directory is not writable the exception will thrown. */ private void ensureDirIsWritable(File directory) throws WorkflowRepoException { if (!directory.canWrite()) { throw new WorkflowRepoException( "Workflow repository is not writable, path: " + directory.getAbsolutePath()); } } private boolean checkRepoMightBeCreated(File dir) throws WorkflowRepoException { if (dir.isFile()) return false; if (dir.listFiles().length != 0) throw new WorkflowRepoException( "Directory is not empty, Git repository cannot be created in: " + dir.getAbsolutePath()); return true; } private void validateGitRepo() throws WorkflowRepoException, IOException { Status stat = null; try { stat = newGit().status().call(); } catch (NoWorkTreeException e) { throw new WorkflowRepoException(e.getMessage(), e); } if (!stat.isClean()) { StringBuilder sb = prepareErrMsg(stat); throw new WorkflowRepoException(sb.toString()); } } private StringBuilder prepareErrMsg(Status stat) { StringBuilder sb = new StringBuilder(); sb.append("\n---------"); if (!stat.getAdded().isEmpty()) { sb.append("\n * uncommited files:") .append(stat.getAdded()).append("."); } if (!stat.getChanged().isEmpty()) { sb.append("\n * changed files:") .append(stat.getChanged()).append("."); } if (!stat.getConflicting().isEmpty()) { sb.append("\n * conflicting files:") .append(stat.getConflicting()).append("."); } if (!stat.getMissing().isEmpty()) { sb.append("\n * missing files:") .append(stat.getMissing()).append("."); } if (!stat.getModified().isEmpty()) { sb.append("\n * modified files:") .append(stat.getModified()).append("."); } if (!stat.getRemoved().isEmpty()) { sb.append("\n * removed files:") .append(stat.getRemoved()).append("."); } if (!stat.getUntracked().isEmpty()) { sb.append("\n * untracked files:") .append(stat.getUntracked()).append("."); } return sb.append("\n---------"); } /** * Checks if repository directory exists. Recreates it if * <code>forceCreate</code> is <code>true</code>. * * @param directory * Directory of the repository. * @param forceCreate * Force create directory if doesn't exist. * @throws WorkflowRepoException * If there is a problem with creation directory, the exception * will thrown. */ private void ensureDirExists(File directory, boolean forceCreate) throws WorkflowRepoException { if (directory.exists()) { return; } LOGGER.debug("Repository doesn't exist, trying to create it..."); if (forceCreate && !directory.mkdirs()) { throw new WorkflowRepoException("Workflow repository cannot be created for path: "+ directory.getAbsolutePath()); } if (!directory.exists()) { throw new WorkflowRepoException( "Workflow repository desn't exist and is not forced to be created. Path: "+ directory.getAbsolutePath()); } } @Override public List<String> listWorkflowNames() throws WorkflowRepoException { synchronized (repo) { File[] files = repoDir.listFiles(filenameFilter); List<String> l = new ArrayList<String>(files.length); try { ObjectId revId = repo.resolve(Constants.HEAD); if (revId == null) { throw new WorkflowRepoException("GIT repository does not exists or it does not contains any workflow files."); } TreeWalk tree = new TreeWalk(repo); tree.addTree(new RevWalk(repo).parseTree(revId)); while (tree.next()) { l.add(tree.getNameString()); } } catch (IOException ex) { throw new WorkflowRepoException("Error during listing GIT repository.", ex); } return l; } } @Override public WorkflowVersionInfo saveWorkflow(String workflowName, InputStream is) throws WorkflowRepoException { synchronized (repo) { saveFile(workflowName, is); return addToRepo(workflowName); } } private WorkflowVersionInfo addToRepo(String workflowName) throws WorkflowRepoException { try { newGit().add().addFilepattern(workflowName).call(); return commit("Workflow saved: " + workflowName); } catch (Exception e) { LOGGER.error("Error adding file ({}) to the local repo ({}) : {}", new Object[] { workflowName, repoDir.getAbsolutePath(), e.getMessage() }); LOGGER.error("Exception while adding file to the local repo", e); throw new WorkflowRepoException("Exception while adding file to the local repo", e); } } private WorkflowVersionInfo commit(String message) throws NoHeadException, NoMessageException, UnmergedPathException, ConcurrentRefUpdateException, JGitInternalException, WrongRepositoryStateException { RevCommit res = newGit().commit().setMessage(message).call(); return new GitVersionInfo(res); } private void saveFile(String workflowName, InputStream is) throws WorkflowRepoException { File f = new File(repoDir, workflowName); FileOutputStream os = null; try { if (!f.exists()) { f.createNewFile(); } os = new FileOutputStream(f); IOUtils.copy(is, os); } catch (IOException e) { LOGGER.error("Error saving file ({}) in the working directory ({}) : {}", new Object[] { workflowName, repoDir.getAbsolutePath(), e.getMessage() }); LOGGER.error("Exception while saving worflow in the working directory", e); throw new WorkflowRepoException("Exception while saving worflow in the working directory", e); } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); } } @Override public List<WorkflowVersionInfo> getVersions(String workflowName) throws WorkflowRepoException { try { Iterable<RevCommit> res; synchronized (repo) { LogCommand cmd = newGit().log(); cmd.addPath(workflowName); res = cmd.call(); } List<WorkflowVersionInfo> list = new ArrayList<WorkflowVersionInfo>(); for (RevCommit rc : res) { list.add(new GitVersionInfo(rc)); } return list; } catch (Exception e) { LOGGER.error("Error listing commits for {}", workflowName); throw new WorkflowRepoException("Error listing commits for "+ workflowName, e); } } @Override public InputStream getWorkflowFile(String workflowName, String version) throws WorkflowRepoException { synchronized (repo) { checkout(workflowName, version); File f = new File(repoDir, workflowName); try { return new FileInputStream(f); } catch (FileNotFoundException e) { throw new WorkflowRepoException("No such file: " + workflowName, null); } // finally{ // checkout(workflowName, "HEAD"); // } } } private void checkout(String workflowName, String version) throws WorkflowRepoException { CheckoutCommand cmd = newGit().checkout(); cmd.addPath(workflowName); cmd.setStartPoint(version); try { cmd.call(); } catch (Exception e) { LOGGER.error("Error checking out {}, rev {}", workflowName, version == null ? "HEAD" : version); throw new WorkflowRepoException("Error performing checkout "+ workflowName, e); } } private Git newGit() { return new Git(repo); } }