All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.subversion.notifications.NotificationsManager Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.netbeans.modules.subversion.notifications;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JTextPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import org.netbeans.modules.subversion.FileInformation;
import org.netbeans.modules.subversion.FileStatusCache;
import org.netbeans.modules.subversion.Subversion;
import org.netbeans.modules.subversion.client.SvnClient;
import org.netbeans.modules.subversion.client.SvnClientExceptionHandler;
import org.netbeans.modules.subversion.ui.diff.DiffAction;
import org.netbeans.modules.subversion.ui.diff.Setup;
import org.netbeans.modules.subversion.ui.history.SearchHistoryAction;
import org.netbeans.modules.subversion.util.Context;
import org.netbeans.modules.subversion.util.SvnUtils;
import org.netbeans.modules.versioning.util.VCSNotificationDisplayer;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakSet;
import org.tigris.subversion.svnclientadapter.ISVNInfo;
import org.tigris.subversion.svnclientadapter.ISVNStatus;
import org.tigris.subversion.svnclientadapter.SVNClientException;
import org.tigris.subversion.svnclientadapter.SVNRevision;
import org.tigris.subversion.svnclientadapter.SVNStatusKind;
import org.tigris.subversion.svnclientadapter.SVNUrl;

/**
 * Notifies about external changes on the given files.
 * @author Ondra Vrabec
 */
public class NotificationsManager {

    private static NotificationsManager instance;
    private static final Logger LOG = Logger.getLogger(NotificationsManager.class.getName());
    private static final Set alreadySeen = Collections.synchronizedSet(new WeakSet());
    private static final String CMD_DIFF = "cmd.diff";                  //NOI18N

    private final HashSet files;
    private final RequestProcessor rp;
    private final RequestProcessor.Task notificationTask;
    private final FileStatusCache cache;
    private Boolean enabled;

    private Map notifiedFiles = Collections.synchronizedMap(new HashMap());

    private NotificationsManager () {
        files = new HashSet();
        rp = new RequestProcessor("SubversionNotifications", 1, true);  //NOI18N
        notificationTask = rp.create(new NotificationTask());
        cache = Subversion.getInstance().getStatusCache();
    }

    /**
     * Returns an instance of the class
     * @return
     */
    public static synchronized NotificationsManager getInstance() {
        if (instance == null) {
            instance = new NotificationsManager();
        }
        return instance;
    }

    /**
     * Plans a task detecting if a notification for the file is needed and in that case displaying the notification.
     * The notification is displayed if the file is still up-to-date (during the time of the call) and there's an external change
     * in the repository.
     * @param file file to scan
     */
    public void scheduleFor(File file) {
        if (isSeen(file) || !isUpToDate(file) || !isEnabled(file)) {
            if (LOG.isLoggable(Level.FINER)) {
                LOG.log(Level.FINER, "File {0} is {1} up to date, notifications enabled: {2}", new Object[]{file.getAbsolutePath(), isUpToDate(file) ? "" : "not ", isEnabled(file)}); //NOI18N
            }
            return;
        }
        boolean refresh;
        // register the file for the scan
        synchronized (files) {
            int size = files.size();
            files.add(file);
            refresh = files.size() != size;
        }
        if (refresh) {
            notificationTask.schedule(1000);
        }
    }

    public void notfied(File[] files, Long revision) {
        for (File file : files) {
            notifiedFiles.put(file, revision);
        }
    }

    public void setupPane(JTextPane pane, final File[] files, String fileNames, final File projectDir, final String url, final String revision) {
         String msg = revision == null
                 ? NbBundle.getMessage(NotificationsManager.class, "MSG_NotificationBubble_DeleteDescription", fileNames, CMD_DIFF) //NOI18N
                 : NbBundle.getMessage(NotificationsManager.class, "MSG_NotificationBubble_Description", fileNames, url, CMD_DIFF); //NOI18N
        pane.setText(msg);

        pane.addHyperlinkListener(new HyperlinkListener() {
            @Override
            public void hyperlinkUpdate(HyperlinkEvent e) {
                if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
                    if(CMD_DIFF.equals(e.getDescription())) {
                        Context ctx = new Context(files);
                        DiffAction.diff(ctx, Setup.DIFFTYPE_REMOTE, NbBundle.getMessage(NotificationsManager.class, "LBL_Remote_Changes", projectDir.getName()), false); //NOI18N
                    } else if (revision != null) {
                        try {
                            SearchHistoryAction.openSearch(new SVNUrl(url), projectDir, Long.parseLong(revision));
                        } catch (MalformedURLException ex) {
                            LOG.log(Level.WARNING, null, ex);
                        }
                    }
                }
            }
        });
    }

    private boolean isEnabled (File file) {
        if (enabled == null) {
            // let's leave a possibility to disable the notifications
            enabled = !"false".equals(System.getProperty("subversion.notificationsEnabled", "true")); //NOI18N
        }
        return enabled;
    }

    private boolean isUpToDate(File file) {
        boolean upToDate = false;
        FileInformation info = cache.getCachedStatus(file);
        if (info == null || (info.getStatus() & FileInformation.STATUS_VERSIONED_UPTODATE) != 0 && !info.isDirectory()) {
            upToDate = true;
        }
        return upToDate;
    }

    private boolean isSeen(File file) {
        return alreadySeen.contains(file) || notifiedFiles.containsKey(file);
    }

    private class NotificationTask extends VCSNotificationDisplayer implements Runnable {

        @Override
        public void run() {
            HashSet filesToScan;
            synchronized (files) {
                filesToScan = new HashSet(files);
                files.clear();
            }
            removeDirectories(filesToScan);
            removeSeenFiles(filesToScan);
            removeNotEnabled(filesToScan);
            if (!filesToScan.isEmpty()) {
                scanFiles(filesToScan);
            }
        }

        @Override
        protected void setupPane(JTextPane pane, final File[] files, final File projectDir, final String url, final String revision) {
            NotificationsManager.this.setupPane(pane, files, getFileNames(files), projectDir, url, revision);
        }

        private void removeDirectories (Collection filesToScan) {
            for (Iterator it = filesToScan.iterator(); it.hasNext();) {
                File file = it.next();
                if (!file.isFile()) {
                    it.remove();
                }
            }
        }

        private void removeNotEnabled (Collection filesToScan) {
            for (Iterator it = filesToScan.iterator(); it.hasNext();) {
                File file = it.next();
                if (!isEnabled(file)) {
                    if (LOG.isLoggable(Level.FINER)) {
                        LOG.log(Level.FINER, "File {0} is probably not from kenai, notifications disabled", new Object[] { file.getAbsolutePath() } ); //NOI18N
                    }
                    it.remove();
                }
            }
        }

        private void removeSeenFiles (Collection filesToScan) {
            for (Iterator it = filesToScan.iterator(); it.hasNext();) {
                File file = it.next();
                if (isSeen(file)) {
                    it.remove();
                }
            }
        }

        private void scanFiles (Collection filesToScan) {
            HashMap> filesPerRepository = sortByRepository(filesToScan);
            for (Map.Entry> entry : filesPerRepository.entrySet()) {
                SVNUrl repositoryUrl = entry.getKey();
                HashMap notifications = new HashMap();
                try {
                    SvnClient client = Subversion.getInstance().getClient(repositoryUrl);
                    if (client != null) {
                        HashSet files = entry.getValue();
                        ISVNStatus[] statuses = client.getStatus(files.toArray(new File[0]));
                        for (ISVNStatus status : statuses) {
                            if ((SVNStatusKind.UNVERSIONED.equals(status.getTextStatus())
                                    || SVNStatusKind.IGNORED.equals(status.getTextStatus()))) {
                                continue;
                            }
                            File file = status.getFile();
                            SVNRevision.Number rev = status.getRevision();
                            // get repository info - last revision if possible
                            ISVNInfo info = null;
                            boolean removed = false;
                            try {
                                SVNUrl url = status.getUrl();
                                if (url == null) {
                                    String canonicalPath;
                                    try {
                                        // i suspect the file to be under a symlink folder
                                        canonicalPath = status.getFile().getCanonicalPath();
                                    } catch (IOException ex) {
                                        canonicalPath = null;
                                    }
                                    LOG.log(Level.WARNING, "scanFiles: though versioned it has no svn url: {0}, {1}, {2}, {3}, {4}", //NOI18N
                                            new Object[] { file, status.getFile(), status.getTextStatus(), status.getUrlString(), canonicalPath });
                                } else {
                                    info = client.getInfo(url, SVNRevision.HEAD, rev);
                                }
                            } catch (SVNClientException ex) {
                                LOG.log(Level.FINE, null, ex);
                                // XXX or should we run remote status to determine if the file was deleted?
                                removed = SvnClientExceptionHandler.isFileNotFoundInRevision(ex.getMessage());
                            }
                            if (info != null || removed) {
                                Long repositoryRev = removed ? null : info.getLastChangedRevision().getNumber();
                                // XXX some hack to look up deleted file's last revision
                                if (isModifiedInRepository(rev.getNumber(), repositoryRev)) {
                                    addToMap(notifications, file, repositoryRev);
                                    // this will refresh versioning view as well
                                    cache.refresh(file, new FileStatusCache.RepositoryStatus(status, null));
                                }
                            }
                        }
                        alreadySeen.addAll(files);
                    }
                } catch (SVNClientException ex) {
                    LOG.log(Level.FINE, null, ex);
                }
                notifyChanges(notifications, repositoryUrl);
            }
        }

        private boolean isModifiedInRepository (long revision, Long repositoryRevision) {
            return repositoryRevision == null // file is probably remotely deleted
                    || revision < repositoryRevision; // base revision is lower than the repository revision
        }

        /**
         * Adds new notification or adds file to already existing notification
         * @param notifications sorted by revision number
         * @param file file to add to a notification
         * @param revision repository revision
         */
        private void addToMap(HashMap notifications, File file, Long revision) {
            Notification revisionNotification = notifications.get(revision);
            if (revisionNotification == null) {
                revisionNotification = new Notification(revision);
                notifications.put(revision, revisionNotification);
            }
            revisionNotification.addFile(file);
        }

        private class Notification {
            private final Set files;
            private final Long revision;

            Notification(Long revision) {
                files = new HashSet();
                this.revision = revision;
            }

            void addFile(File file) {
                files.add(file);
            }

            File[] getFiles () {
                return files.toArray(new File[0]);
            }

            Long getRevision() {
                return revision;
            }
        }

        private HashMap> sortByRepository (Collection files) {
            HashMap> filesByRepository = new HashMap>();
            for (File file : files) {
                SVNUrl repositoryUrl = getRepositoryRoot(file);
                if (repositoryUrl != null) {
                    HashSet filesPerRepository = filesByRepository.get(repositoryUrl);
                    if (filesPerRepository == null) {
                        filesPerRepository = new HashSet();
                        filesByRepository.put(repositoryUrl, filesPerRepository);
                    }
                    filesPerRepository.add(file);
                }
            }
            return filesByRepository;
        }

        private void notifyChanges (HashMap notifications, SVNUrl repositoryUrl) {
            for (Map.Entry e : notifications.entrySet()) {
                Notification notification = e.getValue();
                File[] files = notification.getFiles();
                Long revision = notification.getRevision();
                notifyFileChange(files, files[0].getParentFile(), repositoryUrl.toString(), revision == null ? null : revision.toString());
            }
        }

        /**
         * Returns repository root url for the given file or null if the repository is unknown or does not belong to allowed urls.
         * Currently allowed repositories are kenai repositories.
         * @param file
         * @return
         */
        private SVNUrl getRepositoryRoot (File file) {
            SVNUrl repositoryUrl = null;
            SVNUrl url = getRepositoryUrl(file);
            return repositoryUrl;
        }

        private SVNUrl getRepositoryUrl (File file) {
            SVNUrl url = null;
            try {
                url = SvnUtils.getRepositoryRootUrl(file);
            } catch (SVNClientException ex) {
                LOG.log(Level.FINE, "getRepositoryUrl: No repository root url found for managed file : [" + file + "]", ex); //NOI18N
                try {
                    url = SvnUtils.getRepositoryUrl(file); // try to falback
                } catch (SVNClientException ex1) {
                    LOG.log(Level.FINE, "getRepositoryUrl: No repository url found for managed file : [" + file + "]", ex1); //NOI18N
                }
            }
            return url;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy