com.kenai.redminenb.repository.RedmineRepository.java Source code

Java tutorial

Introduction

Here is the source code for com.kenai.redminenb.repository.RedmineRepository.java

Source

/*
 * Copyright 2012 Anchialas and Mykolaas.
 *
 * Licensed 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 com.kenai.redminenb.repository;

import com.kenai.redminenb.Redmine;
import com.kenai.redminenb.RedmineConfig;
import com.kenai.redminenb.RedmineConnector;
import com.kenai.redminenb.issue.RedmineIssue;
import com.kenai.redminenb.query.RedmineQuery;
import com.kenai.redminenb.query.RedmineQueryController;
import com.kenai.redminenb.user.RedmineUser;

import com.kenai.redminenb.api.AuthMode;
import com.kenai.redminenb.util.ExceptionHandler;
import com.kenai.redminenb.util.NestedProject;
import com.taskadapter.redmineapi.AttachmentManager;
import com.taskadapter.redmineapi.IssueManager;
import com.taskadapter.redmineapi.MembershipManager;
import com.taskadapter.redmineapi.NotFoundException;
import com.taskadapter.redmineapi.ProjectManager;
import com.taskadapter.redmineapi.RedmineException;
import com.taskadapter.redmineapi.RedmineManager;
import com.taskadapter.redmineapi.RedmineManagerFactory;
import com.taskadapter.redmineapi.bean.CustomFieldDefinition;
import com.taskadapter.redmineapi.bean.Issue;
import com.taskadapter.redmineapi.bean.IssueCategory;
import com.taskadapter.redmineapi.bean.IssuePriority;
import com.taskadapter.redmineapi.bean.IssuePriorityFactory;
import com.taskadapter.redmineapi.bean.IssueStatus;
import com.taskadapter.redmineapi.bean.Membership;
import com.taskadapter.redmineapi.bean.Project;
import com.taskadapter.redmineapi.bean.TimeEntryActivity;
import com.taskadapter.redmineapi.bean.TimeEntryActivityFactory;
import com.taskadapter.redmineapi.bean.Tracker;
import com.taskadapter.redmineapi.bean.Version;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.swing.SwingUtilities;
import org.apache.http.conn.ssl.SSLInitializationException;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.modules.bugtracking.spi.RepositoryController;
import org.netbeans.modules.bugtracking.spi.RepositoryInfo;
import org.netbeans.modules.bugtracking.spi.RepositoryProvider;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;

/**
 * Redmine repository manager.
 *
 * @author Mykolas
 * @author Anchialas <anchialas@gmail.com>
 */
@NbBundle.Messages({ "# {0} - repo name", "# {1} - user name", "# {2} - redmine url",
        "LBL_RepositoryTooltip=\"Redmine repository<br>{0} : {1}@{2}" })
public class RedmineRepository {
    private static final Logger LOG = Logger.getLogger(RedmineRepository.class.getName());

    static final String PROPERTY_AUTH_MODE = "authMode"; // NOI18N  
    static final String PROPERTY_ACCESS_KEY = "accessKey"; // NOI18N  
    static final String PROPERTY_PROJECT_ID = "projectId"; // NOI18N
    static final String PROPERTY_FEATURE_WATCHERS = "featureWatchers"; // NOI18N

    private static final List<TimeEntryActivity> fallbackTimeActivityEntries;

    static {
        TimeEntryActivity design = TimeEntryActivityFactory.create(8);
        design.setDefault(false);
        design.setName("Design");
        TimeEntryActivity development = TimeEntryActivityFactory.create(9);
        development.setDefault(false);
        development.setName("Development");
        fallbackTimeActivityEntries = Collections.unmodifiableList(Arrays.asList(design, development));
    }

    private static final List<IssuePriority> fallbackIssuePriorities;

    static {
        fallbackIssuePriorities = Collections
                .unmodifiableList(Arrays.asList(createIssuePriority(7, "Immediate", false),
                        createIssuePriority(6, "Urgent", false), createIssuePriority(5, "High", false),
                        createIssuePriority(4, "Normal", true), createIssuePriority(3, "Low", false)));
    }

    private RepositoryInfo info;
    private transient RepositoryController controller;
    private Map<String, RedmineQuery> queries = null;
    private transient RedmineManager manager;
    private transient RedmineUser currentUser;
    private transient Lookup lookup;
    private final transient InstanceContent ic;

    private final Set<String> issuesToRefresh = new HashSet<>(5);
    private final Set<RedmineQuery> queriesToRefresh = new HashSet<>(3);
    private RequestProcessor.Task refreshIssuesTask;
    private RequestProcessor.Task refreshQueryTask;
    private RequestProcessor requestProcessor;

    private final IssueCache issueCache = new IssueCache(this);

    private final Set<RedmineIssue> newIssues = Collections.synchronizedSet(new HashSet<RedmineIssue>());
    private Map<Integer, NestedProject> projects;
    private final Map<Integer, List<RedmineUser>> userCache = Collections
            .synchronizedMap(new HashMap<Integer, List<RedmineUser>>());
    private final Map<Integer, List<IssueCategory>> categoryCache = Collections
            .synchronizedMap(new HashMap<Integer, List<IssueCategory>>());
    private final Map<Integer, List<Version>> versionCache = Collections
            .synchronizedMap(new HashMap<Integer, List<Version>>());
    private List<IssueStatus> statusCache = null;
    private List<TimeEntryActivity> timeEntryActivityCache = null;
    private List<Tracker> trackerCache = null;
    private List<CustomFieldDefinition> customFieldsCache = null;

    // Make sure we know all instances we created - a crude hack, but API does
    // not allow ourselfes ....
    private static final List<WeakReference<RedmineRepository>> repositoryList = Collections
            .synchronizedList(new LinkedList<WeakReference<RedmineRepository>>());

    {
        repositoryList.add(new WeakReference<>(this));
    }

    public static RedmineRepository getInstanceyById(@NonNull String id) {
        if (id == null) {
            throw new NullPointerException("getInstanceById might not be called with null!");
        }
        synchronized (repositoryList) {
            Iterator<WeakReference<RedmineRepository>> it = repositoryList.iterator();
            RedmineRepository result = null;
            while (it.hasNext()) {
                WeakReference<RedmineRepository> weak = it.next();
                RedmineRepository hard = weak.get();
                if (hard == null) {
                    it.remove();
                } else {
                    if (id.equals(hard.getID()) && result == null) {
                        result = hard;
                    }
                }
            }
            return result;
        }
    }

    /**
     * Default constructor required for deserializing.
     */
    public RedmineRepository() {
        this.ic = new InstanceContent();
    }

    public RedmineRepository(RepositoryInfo info) {
        this();
        this.info = info;
    }

    public IssueCache getIssueCache() {
        return issueCache;
    }

    public RepositoryInfo getInfo() {
        return info;
    }

    synchronized void setInfoValues(String name, String url, String user, char[] password, String accessKey,
            AuthMode authMode, Integer project, boolean featureWatchers) {
        String id = info != null ? info.getID() : name + System.currentTimeMillis();
        String httpUser = null;
        char[] httpPassword = null;
        RepositoryInfo ri = new RepositoryInfo(id, RedmineConnector.ID, url, name, getTooltip(name, user, url),
                user, httpUser, password, httpPassword);
        ri.putValue(PROPERTY_FEATURE_WATCHERS, Boolean.toString(featureWatchers));
        ri.putValue(PROPERTY_PROJECT_ID, project == null ? null : String.valueOf(project));
        info = ri;
        setAccessKey(accessKey);
        setAuthMode(authMode);
        this.projects = null;
    }

    public Map<Integer, NestedProject> getProjects() {
        try {
            if (projects == null) {
                Map<Integer, NestedProject> projectMap = convertProjectList(getProjectManager().getProjects());
                projects = Collections.unmodifiableMap(projectMap);
            }
        } catch (Exception ex) {
            Redmine.LOG.log(Level.WARNING, "Failed to retrieve project list", ex);
        }
        return projects;
    }

    public static Map<Integer, NestedProject> convertProjectList(List<Project> projects) {
        Map<Integer, NestedProject> projectMap = new HashMap<>();
        for (Project p : projects) {
            projectMap.put(p.getId(), new NestedProject(p));
        }
        for (NestedProject np : projectMap.values()) {
            Project p = np.getProject();
            if (p.getParentId() != null) {
                np.setParent(projectMap.get(p.getParentId()));
            }
        }
        return projectMap;
    }

    public String getDisplayName() {
        if (info == null) {
            return "";
        }
        return info.getDisplayName();
    }

    private String getTooltip(String repoName, String user, String url) {
        return Bundle.LBL_RepositoryTooltip(repoName, user, url);
    }

    public String getID() {
        return info.getID();
    }

    public String getUrl() {
        if (info == null) {
            return "";
        }
        return info.getUrl();
    }

    public AuthMode getAuthMode() {
        if (info == null) {
            return AuthMode.AccessKey;
        }
        return AuthMode.get(info.getValue(PROPERTY_AUTH_MODE));
    }

    public void setAuthMode(AuthMode authMode) {
        AuthMode old = getAuthMode();
        if (!Objects.equals(old, authMode)) {
            manager = null;
        }
        info.putValue(PROPERTY_AUTH_MODE, authMode == null ? null : authMode.name());
    }

    public String getAccessKey() {
        if (info == null) {
            return "";
        }
        return info.getValue(PROPERTY_ACCESS_KEY);
    }

    public void setAccessKey(String accessKey) {
        String old = getAccessKey();
        if (!Objects.equals(old, accessKey)) {
            manager = null; // force reconnect
        }
        info.putValue(PROPERTY_ACCESS_KEY, accessKey);
    }

    public char[] getPassword() {
        if (info == null) {
            return new char[0];
        }
        return info.getPassword();
    }

    public String getUsername() {
        if (info == null) {
            return "";
        }
        return info.getUsername();
    }

    public Project getProject() throws RedmineException {
        Integer projectId = getProjectID();
        if (projectId != null) {
            NestedProject np = getProjects().get(projectId);
            if (np != null) {
                return np.getProject();
            }
        }
        return null;
    }

    public Integer getProjectID() {
        try {
            String projectString = info.getValue(PROPERTY_PROJECT_ID);
            return Integer.valueOf(projectString);
        } catch (NullPointerException | NumberFormatException ex) {
            // Accept the potential slow path - the assumption is, that
            // in 99.9...% of all cases a projectId is set - only
            // while constructing a new repository info, this could be empty
            return null;
        }
    }

    public RedmineIssue getIssue(String issueId) {
        RedmineIssue redmineIssue = null;
        if (issueId != null) {
            redmineIssue = issueCache.get(issueId);
            if (redmineIssue == null) {
                try {
                    Issue issue = getIssueManager().getIssueById(Integer.valueOf(issueId));
                    redmineIssue = issueCache.cachedRedmineIssue(issue);
                } catch (NotFoundException ex) {
                    // do nothing
                } catch (Exception ex) {
                    Redmine.LOG.log(Level.SEVERE, null, ex);
                }
            }
        }
        return redmineIssue;
    }

    public Collection<RedmineIssue> getIssues(final String... ids) {
        final List<RedmineIssue> ret = new ArrayList<>(ids.length);
        for (String id : ids) {
            RedmineIssue issue = getIssue(id);
            if (issue != null) {
                ret.add(issue);
            }
        }
        return ret;
    }

    public void remove() {
    }

    synchronized void resetRepository(boolean keepConfiguration) {
        if (!keepConfiguration) {
            manager = null;
        }
    }

    public RepositoryController getController() {
        if (controller == null) {
            controller = new RedmineRepositoryController(this);
        }
        return controller;
    }

    public RedmineIssue createIssue(String summary, String description) {
        RedmineIssue issue = new RedmineIssue(this, summary, description);
        newIssues.add(issue);
        return issue;
    }

    public RedmineIssue createIssue() {
        RedmineIssue issue = new RedmineIssue(this);
        newIssues.add(issue);
        return issue;
    }

    boolean canAttachFiles() {
        return true;
    }

    public RedmineQuery createQuery() {
        return new RedmineQuery(this);
    }

    private synchronized Map<String, RedmineQuery> getQueryMap() {
        if (queries == null) {
            queries = Collections.synchronizedMap(new HashMap<String, RedmineQuery>());
            String[] qs = RedmineConfig.getInstance().getQueries(getID());
            for (String queryName : qs) {
                RedmineQuery q = RedmineConfig.getInstance().getQuery(this, queryName);
                if (q != null) {
                    queries.put(queryName, q);
                } else {
                    Redmine.LOG.log(Level.WARNING, "Couldn''t find query with stored name {0}", queryName); // NOI18N
                }
            }
        }
        return queries;
    }

    public void removeQuery(String displayName) {
        RedmineConfig.getInstance().removeQuery(this, displayName);
        getQueryMap().remove(displayName);
        fireQueryListChanged();

    }

    public void saveQuery(RedmineQuery query) {
        assert info != null;
        RedmineConfig.getInstance().putQuery(this, query);
        getQueryMap().put(query.getDisplayName(), query);
        fireQueryListChanged();
    }

    private void fireQueryListChanged() {
        Redmine.LOG.log(Level.FINER, "firing query list changed for repository {0}",
                new Object[] { getDisplayName() }); // NOI18N
        propertyChangeSupport.firePropertyChange(RepositoryProvider.EVENT_QUERY_LIST_CHANGED, null, null);
    }

    public Collection<RedmineQuery> getQueries() {
        return getQueryMap().values();
    }

    public Collection<RedmineUser> getUsers(Project p) {
        if (p != null && (!userCache.containsKey(p.getId()))) {
            ArrayList<RedmineUser> users = new ArrayList<>();
            try {
                users.add(currentUser);
                for (Membership m : getMembershipManager().getMemberships(p.getId().toString())) {
                    if (m.getUser() != null && !currentUser.getUser().getId().equals(m.getUser().getId())) {
                        users.add(new RedmineUser(m.getUser()));
                    }
                }
            } catch (RedmineException | RuntimeException ex) {
                ExceptionHandler.handleException(LOG, "Can't get Redmine Users", ex);
            }
            userCache.put(p.getId(), Collections.unmodifiableList(users));
        }
        if (p == null || (!userCache.containsKey(p.getId()))) {
            return Collections.EMPTY_LIST;
        }
        return userCache.get(p.getId());
    }

    public List<Tracker> getTrackers() {
        if (trackerCache == null) {
            try {
                trackerCache = getIssueManager().getTrackers();
            } catch (RedmineException | RuntimeException ex) {
                ExceptionHandler.handleException(LOG, "Can't get Redmine Issue Trackers", ex);
            }
        }
        return trackerCache;
    }

    public List<TimeEntryActivity> getTimeEntryActivities() {
        if (timeEntryActivityCache == null) {
            try {
                timeEntryActivityCache = getIssueManager().getTimeEntryActivities();
            } catch (RedmineException | RuntimeException ex) {
                LOG.log(Level.INFO,
                        "Failed to Redmine Time Entry Activities (either API is missing or no permission)", ex);
                timeEntryActivityCache = fallbackTimeActivityEntries;
            }
        }
        return timeEntryActivityCache;
    }

    public IssueStatus getStatus(int id) {
        for (IssueStatus issueStatus : getStatuses()) {
            if (id == issueStatus.getId()) {
                return issueStatus;
            }
        }
        return null;
    }

    public Collection<? extends IssueStatus> getStatuses() {
        if (statusCache == null) {
            try {
                statusCache = getIssueManager().getStatuses();
            } catch (NotFoundException ex) {
                DialogDisplayer.getDefault()
                        .notify(new NotifyDescriptor.Message(
                                "Can't get Issue Statuses from Redmine:\n" + ex.getMessage(),
                                NotifyDescriptor.ERROR_MESSAGE));
                Redmine.LOG.log(Level.SEVERE, "Can't get Issue Statuses from Redmine", ex);
            } catch (Exception ex) {
                Redmine.LOG.log(Level.SEVERE, "Can't get Issue Statuses from Redmine", ex);
            }
        }

        return statusCache;
    }

    public Collection<? extends IssueCategory> reloadIssueCategories(Project p) {
        categoryCache.remove(p.getId());
        return getIssueCategories(p);
    }

    public Collection<? extends IssueCategory> getIssueCategories(Project p) {
        if (p != null && (!categoryCache.containsKey(p.getId()))) {
            try {
                List<IssueCategory> cats = getIssueManager().getCategories(p.getId());
                for (IssueCategory ic : cats) {
                    ic.setProject(null);
                    ic.setAssignee(null);
                }
                categoryCache.put(p.getId(), Collections.unmodifiableList(cats));
            } catch (NotFoundException ex) {
                DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(
                        "Can't get Issue Categories for Redmine Project " + p.getName() + ":\n" + ex.getMessage(),
                        NotifyDescriptor.ERROR_MESSAGE));
                Redmine.LOG.log(Level.SEVERE, "Can't get Issue Categories for Redmine Project " + p.getName(), ex);
            } catch (Exception ex) {
                Redmine.LOG.log(Level.SEVERE, "Can't get Issue Categories for Redmine Project " + p.getName(), ex);
            }
        }
        if (p == null || (!categoryCache.containsKey(p.getId()))) {
            return Collections.EMPTY_LIST;
        }
        return categoryCache.get(p.getId());
    }

    public Collection<? extends Version> reloadVersions(Project p) {
        versionCache.remove(p.getId());
        return getVersions(p);
    }

    public List<Version> getVersions(Project p) {
        if (p != null && (!versionCache.containsKey(p.getId()))) {
            try {
                versionCache.put(p.getId(), getProjectManager().getVersions(p.getId()));
            } catch (Exception ex) {
                Redmine.LOG.log(Level.SEVERE, "Can't get versions for project " + p.getName(), ex);
            }
        }
        if (p == null || (!versionCache.containsKey(p.getId()))) {
            return Collections.EMPTY_LIST;
        }
        return versionCache.get(p.getId());
    }

    public List<IssuePriority> getIssuePriorities() {
        if (issuePriorities == null) {
            try {
                // since Redmine V2.2.0
                issuePriorities = getIssueManager().getIssuePriorities();
                Collections.reverse(issuePriorities);
            } catch (Exception ex) {
                // LOG on info level, as SEVERE causes 
                issuePriorities = fallbackIssuePriorities;
                Redmine.LOG.log(Level.INFO, "Can't get issue priorities, using defaults", ex);
            }
        }
        return issuePriorities;
    }

    public void initCustomFieldDefinitions() {
        if (customFieldsCache == null) {
            try {
                // since Redmine V2.4.0
                customFieldsCache = getManager().getCustomFieldManager().getCustomFieldDefinitions();
            } catch (Exception ex) {
                LOG.info("Custom Fields are not available - query failed");
                customFieldsCache = Collections.EMPTY_LIST;
            }
        }
    }

    public List<CustomFieldDefinition> getCustomFieldDefinitions(String type, Project proj, Tracker t) {
        initCustomFieldDefinitions();
        List<CustomFieldDefinition> result = new ArrayList<>();
        for (CustomFieldDefinition cfd : customFieldsCache) {
            if (type.equals(cfd.getCustomizedType()) && (cfd.getTrackers().contains(t))) {
                // @todo: Rework this not to depend on the string representation
                if ("version".equals(cfd.getFieldFormat())) {
                    cfd.getPossibleValues().clear();
                    for (Version v : getVersions(proj)) {
                        cfd.getPossibleValues().add(v.getName() + " [" + v.getId() + "]");
                    }
                } else if ("user".equals(cfd.getFieldFormat())) {
                    cfd.getPossibleValues().clear();
                    for (RedmineUser ru : getUsers(proj)) {
                        cfd.getPossibleValues().add(ru.getUser().getFullName() + " [" + ru.getId() + "]");
                    }
                }
                result.add(cfd);
            }
        }
        return result;
    }

    public CustomFieldDefinition getCustomFieldDefinitionById(int id) {
        initCustomFieldDefinitions();
        for (CustomFieldDefinition cfd : customFieldsCache) {
            if (cfd.getId().equals(id)) {
                return cfd;
            }
        }
        return null;
    }

    public Collection<RedmineIssue> simpleSearch(String string) {
        try {
            IssueManager issueManager = getIssueManager();
            List<Issue> resultIssues = new LinkedList<>();

            try {
                resultIssues.add(issueManager.getIssueById(Integer.parseInt(string)));
            } catch (NumberFormatException ex) {
            } catch (NotFoundException ex) {
            }

            resultIssues.addAll(issueManager.getIssuesBySummary(null, "*" + string + "*"));

            List<RedmineIssue> redmineIssues = new LinkedList<>();
            for (Issue issue : resultIssues) {
                RedmineIssue redmineIssue = issueCache.cachedRedmineIssue(issue);
                redmineIssues.add(redmineIssue);
            }
            return redmineIssues;
        } catch (RedmineException | RuntimeException ex) {
            ExceptionHandler.handleException(LOG, "Can't search for Redmine issues", ex);
        }
        return Collections.<RedmineIssue>emptyList();
    }

    public Lookup getLookup() {
        if (lookup == null) {
            lookup = new AbstractLookup(ic);
        }
        return lookup;
    }

    public final RedmineManager getManager() throws RedmineException {
        assert (!SwingUtilities.isEventDispatchThread()) : "Access to Redmine Manager must happen outside EDT!";
        AuthMode authMode = getAuthMode();
        if (manager == null) {
            if (authMode == null) {
                throw new IllegalArgumentException("authMode must be set");
            }
            if (authMode == AuthMode.AccessKey) {
                manager = RedmineManagerFactory.createWithApiKey(getUrl(), getAccessKey(),
                        RedmineManagerFactory.createShortTermConfig(createConnectionManager()));
            } else {
                manager = RedmineManagerFactory.createWithUserAuth(getUrl(), getUsername(),
                        getPassword() == null ? "" : String.valueOf(getPassword()),
                        RedmineManagerFactory.createShortTermConfig(createConnectionManager()));
            }
            currentUser = new RedmineUser(manager.getUserManager().getCurrentUser(), true);
            manager.setObjectsPerPage(100);
        }
        return manager;
    }

    static PoolingClientConnectionManager createConnectionManager() throws SSLInitializationException {
        SSLSocketFactory socketFactory = SSLSocketFactory.getSystemSocketFactory();
        socketFactory.setHostnameVerifier(new X509HostnameVerifier() {
            @Override
            public void verify(String string, SSLSocket ssls) throws IOException {
                if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(string, ssls.getSession())) {
                    throw new SSLException("Hostname did not verify");
                }
            }

            @Override
            public void verify(String string, X509Certificate xc) throws SSLException {
                throw new SSLException("Check not implemented yet");
            }

            @Override
            public void verify(String string, String[] strings, String[] strings1) throws SSLException {
                throw new SSLException("Check not implemented yet");
            }

            @Override
            public boolean verify(String string, SSLSession ssls) {
                return HttpsURLConnection.getDefaultHostnameVerifier().verify(string, ssls);
            }
        });
        PoolingClientConnectionManager connectionManager = RedmineManagerFactory
                .createConnectionManager(Integer.MAX_VALUE, socketFactory);
        return connectionManager;
    }

    public IssueManager getIssueManager() throws RedmineException {
        return getManager().getIssueManager();
    }

    public AttachmentManager getAttachmentManager() throws RedmineException {
        return getManager().getAttachmentManager();
    }

    public ProjectManager getProjectManager() throws RedmineException {
        return getManager().getProjectManager();
    }

    public MembershipManager getMembershipManager() throws RedmineException {
        return getManager().getMembershipManager();
    }

    public RedmineUser getCurrentUser() {
        return currentUser;
    }

    public RequestProcessor getRequestProcessor() {
        if (requestProcessor == null) {
            requestProcessor = new RequestProcessor("Redmine repository processor - " + getDisplayName(), 1, true); // NOI18N
        }
        return requestProcessor;
    }

    private void setupIssueRefreshTask() {
        if (refreshIssuesTask == null) {
            refreshIssuesTask = getRequestProcessor().create(new Runnable() {
                @Override
                public void run() {
                    Set<String> ids;
                    synchronized (issuesToRefresh) {
                        ids = new HashSet<String>(issuesToRefresh);
                    }
                    if (ids.isEmpty()) {
                        Redmine.LOG.log(Level.FINE, "no issues to refresh {0}", getDisplayName()); // NOI18N
                        return;
                    }
                    Redmine.LOG.log(Level.FINER, "preparing to refresh issue {0} - {1}",
                            new Object[] { getDisplayName(), ids }); // NOI18N
                    scheduleIssueRefresh();
                }
            });
            scheduleIssueRefresh();
        }
    }

    private void setupQueryRefreshTask() {
        if (refreshQueryTask == null) {
            refreshQueryTask = getRequestProcessor().create(new Runnable() {
                @Override
                public void run() {
                    try {
                        Set<RedmineQuery> queries;
                        synchronized (refreshQueryTask) {
                            queries = new HashSet<RedmineQuery>(queriesToRefresh);
                        }
                        if (queries.isEmpty()) {
                            Redmine.LOG.log(Level.FINE, "no queries to refresh {0}",
                                    new Object[] { getDisplayName() }); // NOI18N
                            return;
                        }
                        for (RedmineQuery q : queries) {
                            Redmine.LOG.log(Level.FINER, "preparing to refresh query {0} - {1}",
                                    new Object[] { q.getDisplayName(), getDisplayName() }); // NOI18N
                            RedmineQueryController qc = q.getController();
                            qc.autoRefresh();
                        }
                    } finally {
                        scheduleQueryRefresh();
                    }
                }
            });
            scheduleQueryRefresh();
        }
    }

    private void scheduleIssueRefresh() {
        int delay = RedmineConfig.getInstance().getIssueRefreshInterval();
        Redmine.LOG.log(Level.FINE, "scheduling issue refresh for repository {0} in {1} minute(s)",
                new Object[] { getDisplayName(), delay }); // NOI18N
        if (delay < 5 && System.getProperty("netbeans.t9y.redmine.force.refresh.delay") == null) { // t9y: Testability
            Redmine.LOG.log(Level.WARNING, " wrong issue refresh delay {0}. Falling back to default {0}",
                    new Object[] { delay, RedmineConfig.DEFAULT_ISSUE_REFRESH }); // NOI18N
            delay = RedmineConfig.DEFAULT_ISSUE_REFRESH;
        }
        refreshIssuesTask.schedule(delay * 60 * 1000); // given in minutes
    }

    private void scheduleQueryRefresh() {
        String schedule = System.getProperty("netbeans.t9y.redmine.force.refresh.schedule", "");
        if (!schedule.isEmpty()) {
            int delay = Integer.parseInt(schedule);
            refreshQueryTask.schedule(delay);
            return;
        }

        int delay = RedmineConfig.getInstance().getQueryRefreshInterval();
        Redmine.LOG.log(Level.FINE, "scheduling query refresh for repository {0} in {1} minute(s)",
                new Object[] { getDisplayName(), delay }); // NOI18N
        if (delay < 5) {
            Redmine.LOG.log(Level.WARNING, " wrong query refresh delay {0}. Falling back to default {0}",
                    new Object[] { delay, RedmineConfig.DEFAULT_QUERY_REFRESH }); // NOI18N
            delay = RedmineConfig.DEFAULT_QUERY_REFRESH;
        }
        refreshQueryTask.schedule(delay * 60 * 1000); // given in minutes
    }

    public void stopRefreshing(String id) {
        Redmine.LOG.log(Level.FINE, "removing issue {0} from refresh on repository {1}",
                new Object[] { id, getDisplayName() }); // NOI18N
        synchronized (issuesToRefresh) {
            issuesToRefresh.remove(id);
        }
    }

    public void scheduleForRefresh(String id) {
        Redmine.LOG.log(Level.FINE, "scheduling issue {0} for refresh on repository {0}",
                new Object[] { id, getDisplayName() }); // NOI18N
        synchronized (issuesToRefresh) {
            issuesToRefresh.add(id);
        }
        setupIssueRefreshTask();
    }

    public void scheduleForRefresh(RedmineQuery query) {
        Redmine.LOG.log(Level.FINE, "scheduling query {0} for refresh on repository {1}",
                new Object[] { query.getDisplayName(), getDisplayName() }); // NOI18N
        synchronized (queriesToRefresh) {
            queriesToRefresh.add(query);
        }
        setupQueryRefreshTask();
    }

    public void stopRefreshing(RedmineQuery query) {
        Redmine.LOG.log(Level.FINE, "removing query {0} from refresh on repository {1}",
                new Object[] { query.getDisplayName(), getDisplayName() }); // NOI18N
        synchronized (queriesToRefresh) {
            queriesToRefresh.remove(query);
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + getDisplayName() + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof RedmineRepository)) {
            return false;
        }
        RedmineRepository other = (RedmineRepository) obj;
        return Objects.equals(this.getDisplayName(), other.getDisplayName())
                && Objects.equals(this.getUrl(), other.getUrl())
                && Objects.equals(getProjectID(), other.getProjectID());
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 97 * hash + (this.getDisplayName() != null ? this.getDisplayName().hashCode() : 0);
        hash = 97 * hash + (this.getUrl() != null ? this.getUrl().hashCode() : 0);
        hash = 97 * hash + (getProjectID() != null ? this.getProjectID().hashCode() : 0);
        return hash;
    }

    // 
    // Change Support
    //
    private transient final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    public boolean isFeatureWatchers() {
        if (info == null) {
            return false;
        }
        String supportsWatchers = info.getValue(PROPERTY_FEATURE_WATCHERS);
        if (supportsWatchers == null) {
            return false;
        } else {
            return Boolean.parseBoolean(supportsWatchers);
        }
    }

    public IssuePriority getDefaultIssuePriority() {
        for (IssuePriority issuePriority : getIssuePriorities()) {
            if (issuePriority.isDefault()) {
                return issuePriority;
            }
        }
        return null;
    }

    public static IssuePriority createIssuePriority(Integer id, String name, boolean isDefault) {
        IssuePriority ip = IssuePriorityFactory.create(id);
        ;
        ip.setName(name);
        ip.setDefault(isDefault);
        return ip;
    }

    private List<IssuePriority> issuePriorities;

    public IssuePriority getIssuePriority(Integer id) {
        for (IssuePriority ip : getIssuePriorities()) {
            if (ip.getId().equals(id)) {
                return ip;
            }
        }
        return null;
    }
}