org.headsupdev.agile.app.files.BrowseScmUpdater.java Source code

Java tutorial

Introduction

Here is the source code for org.headsupdev.agile.app.files.BrowseScmUpdater.java

Source

/*
 * HeadsUp Agile
 * Copyright 2009-2013 Heads Up Development Ltd.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.headsupdev.agile.app.files;

import org.headsupdev.support.java.StringUtil;
import org.headsupdev.agile.api.*;
import org.headsupdev.agile.scm.HeadsUpScmManager;
import org.headsupdev.agile.scm.ScmVariant;
import org.headsupdev.agile.storage.*;
import org.headsupdev.agile.storage.ScmChangeSet;
import org.headsupdev.agile.app.files.event.FileChangeSetEvent;
import org.headsupdev.agile.api.logging.Logger;

import org.apache.maven.scm.*;
import org.apache.maven.scm.repository.ScmRepository;
import org.apache.maven.scm.command.changelog.ChangeLogSet;
import org.apache.maven.scm.command.diff.DiffScmResult;
import org.apache.maven.scm.command.update.UpdateScmResult;
import org.headsupdev.support.java.FileUtil;
import org.hibernate.Transaction;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;

import java.io.File;
import java.util.*;

/**
 * A class used to run the scm updater thread - this keeps projects up to date and loads the metadata about the changes.
 *
 * @author Andrew Williams
 * @version $Id$
 * @since 1.0
 */
public class BrowseScmUpdater implements ProjectListener {
    private HeadsUpScmManager scmManager = HeadsUpScmManager.getInstance();

    private Thread updater;
    private List<String> pendingUpdates = new LinkedList<String>();
    private boolean shutdown = false;
    private boolean updating = false;

    private Logger log = Manager.getLogger(getClass().getName());

    public BrowseScmUpdater() {
        updater = new Thread("PollingFileUpdater") {
            public void run() {
                // wait before starting
                try {
                    Thread.sleep(getUpdateDelay());
                } catch (InterruptedException e) {
                    // ignore and run anyway I guess
                }

                while (!shutdown) {
                    try {
                        // add each (not currently updating) project to the list again and make sure we are updating
                        for (Project project : Manager.getStorageInstance().getRootProjects()) {
                            queueProject(project);
                        }
                        updateProjects();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    try {
                        Thread.sleep(getUpdateDelay());
                    } catch (InterruptedException e) {
                        // ignore and run anyway I guess
                    }
                }
            }

        };

        updater.start();
    }

    protected int getUpdateDelay() {
        int delay = (Integer) BrowseApplication.CONFIGURATION_UPDATE_DELAY.getDefault();
        String delayStr = Manager.getStorageInstance().getGlobalConfiguration()
                .getApplicationConfiguration(BrowseApplication.ID)
                .getProperty(BrowseApplication.CONFIGURATION_UPDATE_DELAY.getKey());
        if (delayStr != null) {
            delay = Integer.parseInt(delayStr);
        }

        return delay * 1000 * 60;
    }

    public void stop() {
        shutdown = true;
        updater.interrupt();
    }

    public void updateAll() {
        updater.interrupt();
    }

    public void updateProject(Project project) {
        if (project.equals(StoredProject.getDefault())) {
            updateAll();
        } else {
            queueProject(project);
            updateProjects();
        }
    }

    public void queueProject(Project project) {
        if (!pendingUpdates.contains(project.getId())) {
            if (project.getParent() == null) {
                pendingUpdates.add(project.getId());
            }
        }
    }

    protected void updateProjects() {
        if (updating) {
            return;
        }

        new Thread("ManualFileUpdater") {
            @Override
            public void run() {
                doUpdateProjects();
            }
        }.start();
    }

    protected void doUpdateProjects() {
        if (updating) {
            return;
        }

        Task check = new CheckUpdateTask();
        Manager.getInstance().addTask(check);
        updating = true;
        while (pendingUpdates.size() > 0) {
            String next = pendingUpdates.remove(0);

            loadChangeSets(next);
        }
        updating = false;
        Manager.getInstance().removeTask(check);
    }

    // TODO if we shut down whilst this is running we need to resume when we restart
    // (currently it waits for the next update before it sees we have changes to load)
    private void loadChangeSets(String projectId) {
        Project project = getProject(projectId);
        String scm = project.getScm();
        File working = Manager.getStorageInstance().getWorkingDirectory(project);
        boolean importing = false;
        boolean first = false;

        UpdateScmResult result;
        // this date is set in the future so we get all changes from any timezone
        // TODO maybe there is a better way of knowing when the last change for the changeLog is?...
        Date end = new Date(System.currentTimeMillis() + (24 * 60 * 60 * 1000));
        String previousId = project.getRevision();
        try {
            result = scmManager.update(scmManager.makeScmRepository(scm), new ScmFileSet(working), false);

            if (previousId == null || previousId.equals("0")) {
                importing = true;
                first = true;
            } else if (result.getUpdatedFiles() == null || result.getUpdatedFiles().size() == 0) {
                return;
            }
        } catch (Exception e) {
            log.error("Error updating scm copy", e);
            return;
        }

        Task updateTask = new UpdateTask(project);
        try {
            Manager.getInstance().addTask(updateTask);

            Session session = HibernateUtil.openSession();
            Transaction tx = session.beginTransaction();

            org.headsupdev.agile.api.service.ChangeSet lastChanges = getChangeSet(project, previousId);
            Date start;
            if (lastChanges != null) {
                start = lastChanges.getDate();
            } else {
                start = new Date(0);
            }

            ScmRepository repository = scmManager.makeScmRepository(scm);
            ScmVariant variant = HeadsUpScmManager.getInstance().getScmVariant(repository.getProvider());

            ChangeLogSet changes = scmManager.changeLog(repository, new ScmFileSet(working), start, end, 0, null)
                    .getChangeLog();
            File loadedWorking = new File(Manager.getStorageInstance().getApplicationDataDirectory(application),
                    project.getId());
            List<ScmFile> checkedOutFiles = null;
            // create a checkout at the latest version we have recorded
            if (!loadedWorking.exists()) {
                repository.getProviderRepository().setPersistCheckout(true);
                if (!StringUtil.isEmpty(project.getScmUsername())) {
                    repository.getProviderRepository().setUser(project.getScmUsername());
                }
                if (!StringUtil.isEmpty(project.getScmPassword())) {
                    repository.getProviderRepository().setPassword(project.getScmPassword());
                }

                loadedWorking.mkdirs();
                if (changes == null || changes.getChangeSets() == null || changes.getChangeSets().size() == 0) {
                    return;
                }

                org.apache.maven.scm.ChangeSet firstChange;
                if (!variant.isLogOldestFirst()) {
                    firstChange = (org.apache.maven.scm.ChangeSet) changes.getChangeSets()
                            .get(changes.getChangeSets().size() - 1);
                } else {
                    firstChange = (org.apache.maven.scm.ChangeSet) changes.getChangeSets().get(0);
                }
                ScmRevision firstRevision = new ScmRevision(
                        ((ChangeFile) firstChange.getFiles().get(0)).getRevision());
                checkedOutFiles = scmManager.checkOut(repository, new ScmFileSet(loadedWorking), firstRevision)
                        .getCheckedOutFiles();
            }
            tx.commit();
            session.close();

            if (changes == null || changes.getChangeSets() == null) {
                return;
            }

            log.info("Found " + (variant.isTransactional() ? "" : "non") + "transactional changeset with "
                    + changes.getChangeSets().size() + " changes");

            List changeListings = changes.getChangeSets();
            if (!variant.isLogOldestFirst()) {
                log.info("reversing list for provider " + repository.getProvider());
                Collections.reverse(changeListings);
            }
            ListIterator changeList = changeListings.listIterator();
            while (changeList.hasNext()) {
                org.apache.maven.scm.ChangeSet changeSet = (org.apache.maven.scm.ChangeSet) changeList.next();

                String revision;
                Date current = changeSet.getDate();
                if (variant.isTransactional()) {
                    revision = changeSet.getRevision();

                    if (revision == null) {
                        // Compatibility code with maven-scm bugs - TODO remove
                        revision = ((ChangeFile) changeSet.getFiles().get(0)).getRevision();
                    }
                } else {
                    revision = changeSet.getAuthor() + ":" + changeSet.getDate();
                }

                // some scms return the revision before this time for completeness...
                if (getChangeSet(project, revision) != null) {
                    // note that here we may have updated info if the scm supports duplicates - i.e. 2 merges into 1 commit...
                    continue;
                }

                // get the status of each updated file. For the first revision this is from a checkout, others an update
                List<ScmChange> changedFiles = new LinkedList<ScmChange>();
                List<ScmFile> updatedFiles = null;
                boolean moved = false;
                if (first) {
                    updatedFiles = checkedOutFiles;
                } else {
                    if (variant.isTransactional()) {
                        UpdateScmResult updateResult = scmManager.update(repository, new ScmFileSet(loadedWorking),
                                new ScmRevision(revision), false);

                        if (updateResult.isSuccess()) {
                            updatedFiles = updateResult.getUpdatedFiles();
                        } else {
                            if (shutdown) {
                                log.warn("Terminating update, not all changes imported");
                                return;
                            }
                            moved = true;

                            log.warn(
                                    "Failed to update, perhaps the repository has moved - re-checking out at the current revision");
                            FileUtil.delete(loadedWorking, true);
                            scmManager.checkOut(repository, new ScmFileSet(loadedWorking),
                                    new ScmRevision(revision));
                        }
                    } else {
                        // TODO fix maven-scm bug for update using Date... (then we can remove the if above)
                        UpdateScmResult updateResult = scmManager.update(repository, new ScmFileSet(loadedWorking),
                                current);
                        updatedFiles = updateResult.getUpdatedFiles();
                    }
                }

                org.headsupdev.agile.api.service.ChangeSet set;
                if (variant.isTransactional()) {
                    set = new TransactionalScmChangeSet(revision, changeSet.getAuthor(), changeSet.getComment(),
                            current, project);
                } else {
                    set = new ScmChangeSet(changeSet.getAuthor(), changeSet.getComment(), current, project);
                }

                log.info("Requesting diff from " + previousId + " to " + revision);
                Set<Project> affected = new HashSet<Project>();
                DiffScmResult diff = null;
                if (variant.isTransactional()) {
                    diff = scmManager.diff(scmManager.makeScmRepository(scm), new ScmFileSet(working),
                            variant.getStartRevisionForDiff(previousId, revision),
                            variant.getEndRevisionForDiff(previousId, revision));
                } else {
                    // TODO fix glaring omission in maven-scm where the diff( Date...Date ) is not supported...
                }
                log.info("Found " + diff.getChangedFiles() + " file diffs");

                boolean shouldNotify = !importing || !changeList.hasNext();
                session = HibernateUtil.openSession();
                tx = session.beginTransaction();
                try {
                    project = (Project) session.merge(project);
                    ((ScmChangeSet) set).setPrevious(lastChanges);

                    // enter the changes with diffs to the database
                    List<ScmFile> scmFiles;
                    if (moved || (!StringUtil.isEmpty(previousId) && variant.useDiffForFileListing())) {
                        // using extended diff is much richer than the update results (but provides nonsense for #1)
                        scmFiles = diff.getChangedFiles();
                    } else {
                        scmFiles = updatedFiles;
                    }

                    if (scmFiles != null) {
                        for (ScmFile scmFile : scmFiles) {
                            int type;
                            if (scmFile.getStatus().equals(ScmFileStatus.ADDED)) {
                                type = ScmChange.TYPE_ADDED;
                            } else if (scmFile.getStatus().equals(ScmFileStatus.DELETED)) {
                                type = ScmChange.TYPE_REMOVED;
                            } else {
                                type = ScmChange.TYPE_CHANGED;
                            }

                            ScmChange adding;
                            // a small hack to match the maven-scm output
                            String path = scmFile.getPath().replace(File.separatorChar, '/');
                            String difference = null;
                            if (diff != null && diff.getDifferences() != null
                                    && diff.getDifferences().containsKey(path)) {
                                difference = diff.getDifferences().get(path).toString();
                            }
                            if (variant.isTransactional()) {
                                adding = new ScmChange(path, type, difference, set);
                            } else {
                                adding = new ScmChange(path, findRevisionForScmFile(changeSet, scmFile), type,
                                        difference, set);
                            }
                            changedFiles.add(adding);
                            set.getChanges().add(adding);
                            session.save(adding);
                        }
                    }

                    session.save(set);
                    log.info("Saved changeset " + set.getId() + " with " + set.getChanges().size() + " files");

                    if (lastChanges != null) {
                        ((ScmChangeSet) lastChanges).setNext(set);
                        session.merge(lastChanges);
                    }

                    // update the file revision links
                    for (ScmChange file : changedFiles) {
                        if (variant.isTransactional()) {
                            affected.add(updateFile(project, file.getName(), revision, session, shouldNotify));
                        } else {
                            affected.add(
                                    updateFile(project, file.getName(), file.getRevision(), session, shouldNotify));
                        }
                    }

                    if (affected.isEmpty()) {
                        affected.add(project);
                    }

                    for (Project affect : affected) {
                        log.info("Setting revision to " + revision + " for project " + affect.getId());
                        setRevision(affect, set.getId(), session);
                    }

                    ScmCommentParser.parseComment(set.getComment(), set);

                    tx.commit();
                } catch (Exception e) {
                    // something failed in the database, log it and try again
                    // TODO find the real cause and remove this (previous(); continue) hack...
                    log.error("Failed to load project change set", e);
                    e.printStackTrace();
                    tx.rollback();
                    //                    changeList.previous();

                    continue;
                } finally {
                    session.close();
                }

                previousId = set.getId();
                lastChanges = set;
                first = false;

                for (Project affect : affected) {
                    application.addEvent(new FileChangeSetEvent(set, affect), shouldNotify);
                }
            }
        } catch (Throwable t) {
            log.error("Error updating projects", t);
        } finally {
            Manager.getInstance().removeTask(updateTask);
        }
    }

    private Project getProject(String id) {
        Session session = HibernateUtil.getCurrentSession();
        Transaction tx = session.beginTransaction();

        Project ret = (Project) session.createCriteria(StoredProject.class).add(Restrictions.idEq(id))
                .uniqueResult();
        session.close();

        return ret;
    }

    private void setRevision(Project project, String revision, Session session) {
        project.setRevision(revision);

        if (project.getParent() != null) {
            setRevision(project.getParent(), revision, session);
        }
    }

    public void projectAdded(Project project) {
        if (project.getParent() == null) {
            queueProject(project);
            updateProjects();
        }
    }

    public void projectModified(Project project) {
    }

    public void projectFileModified(Project project, String path, File file) {
    }

    public void projectRemoved(Project project) {
    }

    protected Project updateFile(Project project, String path, String revision, Session session,
            boolean shouldNotify) {
        File file = new File(path);
        session.merge(new org.headsupdev.agile.storage.files.File(path, revision, project));

        while (file.getParentFile() != null) {
            file = file.getParentFile();

            session.merge(new org.headsupdev.agile.storage.files.File(file.getPath(), revision, project));
        }

        return getChangedProjects(path, project, "", session, shouldNotify);
    }

    private Project getChangedProjects(String path, Project project, String rel, Session session,
            boolean shouldNotify) {
        char sep;
        String test = rel;
        // scm might not end with a slash, but we need it to, but should not start with one
        if (rel.startsWith("\\") || rel.startsWith("/")) {
            test = rel.substring(1);
            sep = rel.charAt(0);

            if (!(rel.endsWith("\\") || rel.endsWith("/"))) {
                test = test + sep;
            }
        }

        if (!path.startsWith(test)) {
            return null;
        }

        for (Project child : project.getChildProjects()) {
            int scmDiff = child.getScm().length() - project.getScm().length();
            Project possible = getChangedProjects(path, child,
                    rel + child.getScm().substring(child.getScm().length() - scmDiff), session, shouldNotify);

            if (possible != null) {
                return possible;
            }
        }

        if (shouldNotify) {
            Project root = project;
            while (root.getParent() != null) {
                root = root.getParent();
            }
            File working = new File(Manager.getStorageInstance().getApplicationDataDirectory(application),
                    root.getId());

            String projectFilePath = path.substring(rel.length());
            File file = new File(working, path);
            Manager.getInstance().fireProjectFileModified(project, projectFilePath, file);
        }
        return project;
    }

    private BrowseApplication application;

    void setApplication(BrowseApplication application) {
        this.application = application;
    }

    public org.headsupdev.agile.api.service.ChangeSet getChangeSet(Project project, String revision) {
        Session session = ((HibernateStorage) Manager.getStorageInstance()).getHibernateSession();
        Transaction tx = session.beginTransaction();

        org.headsupdev.agile.api.service.ChangeSet ret = Manager.getInstance().getScmService().getChangeSet(project,
                revision);
        tx.commit();

        return ret;
    }

    public String findRevisionForScmFile(org.apache.maven.scm.ChangeSet changeSet, ScmFile scmFile) {
        String scmPath = scmFile.getPath();
        String version = "";

        for (ChangeFile changeFile : (List<ChangeFile>) changeSet.getFiles()) {
            if (changeFile.getName().equals(scmPath)) {
                version = changeFile.getRevision();
                break;
            }
        }

        return version;
    }
}