/* * Copyright 2012 Anchialas. * * 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.query; import com.kenai.redminenb.Redmine; import com.kenai.redminenb.RedmineConnector; import com.kenai.redminenb.issue.RedmineIssue; import com.kenai.redminenb.repository.IssueCache; import com.kenai.redminenb.repository.RedmineRepository; import com.kenai.redminenb.util.ExceptionHandler; import com.kenai.redminenb.util.NestedProject; import com.kenai.redminenb.util.SafeAutoCloseable; import com.taskadapter.redmineapi.RedmineException; import com.taskadapter.redmineapi.bean.Issue; import com.taskadapter.redmineapi.bean.Project; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; import org.apache.commons.lang.StringUtils; import org.netbeans.modules.bugtracking.spi.QueryController; import org.netbeans.modules.bugtracking.spi.QueryProvider; import org.openide.util.Mutex; /** * Redmine Query. * * @author Anchialas <anchialas@gmail.com> */ public final class RedmineQuery { private static final Logger LOG = Logger.getLogger(RedmineQuery.class.getName()); private String name; private final RedmineRepository repository; private final Set<RedmineIssue> issues = new HashSet<>(); // private boolean firstRun = true; private boolean saved; private long lastRefresh; private final PropertyChangeSupport support = new PropertyChangeSupport(this); // private Map<String, ParameterValue[]> parameters = new HashMap<>(); private final RedmineQueryController queryController; private Integer busy = 0; private final SafeAutoCloseable busyHelper = new SafeAutoCloseable() { @Override public void close() { setBusy(false); } }; public SafeAutoCloseable busy() { setBusy(true); return busyHelper; } public synchronized boolean isBusy() { return busy != 0; } public synchronized void setBusy(boolean busyBool) { final boolean oldBusy = isBusy(); if (busyBool) { busy++; } else { busy--; } if (busy < 0) { throw new IllegalStateException("Inbalanced busy/nonbusy"); } Mutex.EVENT.writeAccess(new Runnable() { @Override public void run() { support.firePropertyChange("busy", oldBusy, busy != 0); } }); } public RedmineQueryController getController() { return queryController; } public RedmineQuery(RedmineRepository repository) { this.repository = repository; this.queryController = new RedmineQueryController(repository, this); try { Project p = repository.getProject(); NestedProject np = repository.getProjects().get(p.getId()); parameters.put("project_id", new ParameterValue[]{ new ParameterValue(np.toString(), p.getId())}); } catch (NullPointerException ex) { // Happens when failing to retrieve project/no project set => swallow } } public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } private void firePropertyChanged() { support.firePropertyChange(QueryController.PROP_CHANGED, null, null); } public Map<String, ParameterValue[]> getParameters() { return parameters; } public void setParameters(Map<String, ParameterValue[]> parameters) { if(parameters == null) { parameters = Collections.EMPTY_MAP; } boolean changed = ! parameters.equals(this.parameters); this.parameters = parameters; if (changed) { firePropertyChanged(); setSaved(false); } } private QueryProvider.IssueContainer<RedmineIssue> delegateContainer; void setIssueContainer(QueryProvider.IssueContainer<RedmineIssue> ic) { delegateContainer = ic; } public String getDisplayName() { return name; } public String getTooltip() { return name + " - " + repository.getDisplayName(); // NOI18N } public RedmineRepository getRepository() { return repository; } void refresh(boolean autoReresh) { doRefresh(autoReresh); } public void refresh() { // @todo what if already running! - cancel task doRefresh(false); } private boolean doRefresh(final boolean autoRefresh) { // XXX what if already running! - cancel task assert !SwingUtilities.isEventDispatchThread() : "Accessing remote host. Do not call in awt"; // NOI18N final boolean ret[] = new boolean[1]; try(SafeAutoCloseable sac = busy()) { executeQuery(new Runnable() { @Override public void run() { Redmine.LOG.log(Level.FINE, "refresh start - {0}", name); // NOI18N try { if (delegateContainer != null) { delegateContainer.refreshingStarted(); delegateContainer.clear(); } issues.clear(); firstRun = false; try { List<Issue> issueArr = doSearch(); IssueCache issueCache = repository.getIssueCache(); for (Issue issue : issueArr) { RedmineIssue redmineIssue = issueCache.cachedRedmineIssue(issue); issues.add(redmineIssue); if (delegateContainer != null) { delegateContainer.add(redmineIssue); } fireNotifyData(redmineIssue); // XXX - !!! triggers getIssues() } } catch (RedmineException | RuntimeException ex) { ExceptionHandler.handleException(LOG, "Failed to search", ex); } if (delegateContainer != null) { delegateContainer.refreshingFinished(); } } finally { logQueryEvent(issues.size(), autoRefresh); Redmine.LOG.log(Level.FINE, "refresh finish - {0}", name); // NOI18N } } }); } return ret[0]; } protected void logQueryEvent(int count, boolean autoRefresh) { LOG.fine(String.format("Query '%s-%s', Count: %d, Autorefresh: %b", RedmineConnector.NAME, name, count, autoRefresh )); } /** * Performs the issue search with the attributes and values provided by the * map. * <p> * Note: The Redmine REST API does not support full search support for all * fields. So the issues are post filtered here. * * @see http://www.redmine.org/projects/redmine/wiki/Rest_Issues * @see RedmineQueryController#RedmineQueryController * @param searchParameters */ private List<Issue> doSearch() throws RedmineException { boolean searchDescription = false; ParameterValue[] queryStringParameter = parameters.get("query"); String queryStr = ParameterValue.flattenList(queryStringParameter); Map<String, String> m = new HashMap<>(); for (Entry<String,ParameterValue[]> p : parameters.entrySet()) { String parameter = p.getKey(); // Query parameter is handled seperatedly if("query".equals(parameter)) { continue; } ParameterValue[] paramValues = p.getValue(); if (StringUtils.isNotBlank(ParameterValue.flattenList(paramValues))) { if ( "is_subject".equals(parameter) ) { if( StringUtils.isNotBlank(queryStr) ) { m.put("subject", "~" + queryStr); } } else if ("is_description".equals(parameter)) { searchDescription = "1".equals(paramValues[0].getValue()); } else { boolean isNone = false; for (ParameterValue pv : paramValues) { if (ParameterValue.NONE_PARAMETERVALUE.equals(pv)) { isNone = true; } } if (isNone) { m.put(parameter, "!*"); } else if (paramValues.length == 1) { m.put(parameter, paramValues[0].getValue()); } else if (paramValues.length > 1) { if("project_id".equals(parameter)) { m.put(parameter, paramValues[0].getValue()); LOG.warning("Redmine currently (2.6.0) does not allow multiple projects for querying - only using first project"); } else { m.put(parameter, ParameterValue.flattenList(paramValues)); } } } } } List<Issue> issueArr = new ArrayList<>(); // Limit request count int offset = 0; for(int i = 0; i < 100; i++) { // Perform search // According to the documentation 100 is the maximum m.put("limit", Integer.toString(100)); m.put("offset", Integer.toString(offset)); List<Issue> queryResult = repository.getIssueManager().getIssues(m).getResults(); issueArr.addAll(queryResult); offset += queryResult.size(); if(queryResult.isEmpty()) { break; } } // Post filtering: Query string for description if (searchDescription && StringUtils.isNotBlank(queryStr)) { List<Issue> newArr = new ArrayList<>(issueArr.size()); for (Issue issue : issueArr) { if (StringUtils.containsIgnoreCase(issue.getDescription(), queryStr)) { newArr.add(issue); } } issueArr = newArr; } return issueArr; } public void remove() { repository.removeQuery(this.getDisplayName()); firePropertyChanged(); } boolean wasRun() { return !firstRun; } long getLastRefresh() { return lastRefresh; } public void setName(String name) { this.name = name; } public void setSaved(boolean saved) { this.saved = saved; firePropertyChanged(); } public boolean isSaved() { return saved; } public Collection<RedmineIssue> getIssues() { return Collections.unmodifiableSet(issues); } public boolean contains(RedmineIssue issue) { return issues.contains(issue); } public void addNotifyListener(QueryNotifyListener l) { List<QueryNotifyListener> list = getNotifyListeners(); synchronized (list) { list.add(l); } } public void removeNotifyListener(QueryNotifyListener l) { List<QueryNotifyListener> list = getNotifyListeners(); synchronized (list) { list.remove(l); } } protected void fireNotifyData(RedmineIssue issue) { QueryNotifyListener[] listeners = getListeners(); for (QueryNotifyListener l : listeners) { l.notifyData(issue); } } protected void fireStarted() { QueryNotifyListener[] listeners = getListeners(); for (QueryNotifyListener l : listeners) { l.started(); } } protected void fireFinished() { QueryNotifyListener[] listeners = getListeners(); for (QueryNotifyListener l : listeners) { l.finished(); } } // XXX move to API protected void executeQuery(Runnable r) { fireStarted(); try { r.run(); } finally { lastRefresh = System.currentTimeMillis(); fireFinished(); firePropertyChanged(); } } private QueryNotifyListener[] getListeners() { List<QueryNotifyListener> list = getNotifyListeners(); QueryNotifyListener[] listeners; synchronized (list) { listeners = list.toArray(new QueryNotifyListener[list.size()]); } return listeners; } private List<QueryNotifyListener> notifyListeners; private List<QueryNotifyListener> getNotifyListeners() { if (notifyListeners == null) { notifyListeners = new ArrayList<>(); } return notifyListeners; } void rename(String newName) { throw new UnsupportedOperationException("Not supported yet."); } boolean canRename() { return false; } boolean canRemove() { return true; } }