View Javadoc

1   /*
2    * Copyright 2008-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.maven.issues.repository;
17  
18  import java.util.ArrayList;
19  import java.util.HashSet;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.TreeSet;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.apache.maven.reporting.MavenReportException;
28  import org.codehaus.plexus.util.StringUtils;
29  import org.eclipse.core.runtime.CoreException;
30  import org.eclipse.core.runtime.IProgressMonitor;
31  import org.eclipse.core.runtime.IStatus;
32  import org.eclipse.core.runtime.NullProgressMonitor;
33  import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
34  import org.eclipse.mylyn.commons.net.AuthenticationType;
35  import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector;
36  import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
37  import org.eclipse.mylyn.tasks.core.TaskRepository;
38  import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
39  import org.eclipse.mylyn.tasks.core.data.TaskData;
40  import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
41  import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
42  
43  import de.smartics.maven.issues.IssueManagementConfig;
44  import de.smartics.maven.issues.QueryData;
45  import de.smartics.maven.issues.RepositoryFacade;
46  import de.smartics.maven.issues.ResourceLocator;
47  import de.smartics.maven.issues.cache.TaskDataCache;
48  
49  /**
50   * Base implementation of the {@link RepositoryFacade} interface.
51   *
52   * @author <a href="mailto:robert.reiner@smartics.de">Robert Reiner</a>
53   * @version $Revision:591 $
54   */
55  public abstract class AbstractRepositoryFacade implements RepositoryFacade
56  { // NOPMD
57    // ********************************* Fields *********************************
58  
59    // --- constants ------------------------------------------------------------
60  
61    // --- members --------------------------------------------------------------
62  
63    /**
64     * Reference to the logger for this class.
65     */
66    private final Log log = LogFactory.getLog(AbstractRepositoryFacade.class);
67  
68    /**
69     * The configuration for the issue management connection.
70     */
71    protected final IssueManagementConfig config;
72  
73    /**
74     * The connector to use to connect to the remote issue management system.
75     */
76    protected final AbstractRepositoryConnector connector;
77  
78    /**
79     * The reference to the repository.
80     */
81    protected TaskRepository repository;
82  
83    // ****************************** Initializer *******************************
84  
85    // ****************************** Constructors ******************************
86  
87    /**
88     * Default constructor.
89     *
90     * @param config the configuration for the issue management connection.
91     * @param connector the connector to use to connect to the remote issue
92     *          management system.
93     */
94    protected AbstractRepositoryFacade(final IssueManagementConfig config,
95        final AbstractRepositoryConnector connector)
96    {
97      this.config = config;
98      this.connector = connector;
99      this.repository = createRepository();
100   }
101 
102   // ****************************** Inner Classes *****************************
103 
104   /**
105    * A simple collector that accepts every task data and adds them to the task
106    * list.
107    */
108   private static final class SimpleTaskDataCollector extends TaskDataCollector
109   {
110     /**
111      * The list of accepted tasks.
112      */
113     private final List<TaskData> tasks = new ArrayList<TaskData>();
114 
115     /**
116      * Returns the list of accepted tasks.
117      *
118      * @return the list of accepted tasks.
119      */
120     public List<TaskData> getTasks()
121     {
122       return tasks;
123     }
124 
125     /**
126      * {@inheritDoc}
127      * <p>
128      * No filtering: Accepts every task.
129      * </p>
130      *
131      * @see org.eclipse.mylyn.tasks.core.data.TaskDataCollector#accept(org.eclipse.mylyn.tasks.core.data.TaskData)
132      */
133     @Override
134     public void accept(final TaskData taskData)
135     {
136       tasks.add(taskData);
137     }
138 
139   }
140 
141   // ********************************* Methods ********************************
142 
143   // --- init -----------------------------------------------------------------
144 
145   /**
146    * Creates and configures the access to the issues management repository.
147    *
148    * @return the configured issue management repository.
149    */
150   private TaskRepository createRepository()
151   {
152     final TaskRepository newRepository =
153         new TaskRepository(config.getIssueManagementId(),
154             config.getConnectionUrl());
155     newRepository.setProperty(TaskRepository.PROXY_USEDEFAULT, "false");
156 
157     final String repositoryVersion = config.getRepositoryVersion();
158     if (StringUtils.isNotBlank(repositoryVersion))
159     {
160       newRepository.setVersion(repositoryVersion);
161     }
162 
163     setAuthenicationInformation(newRepository);
164     return newRepository;
165   }
166 
167   /**
168    * Sets the authentication information for the given repository.
169    *
170    * @param repository the repository the authenication information is provided
171    *          for.
172    */
173   private void setAuthenicationInformation(final TaskRepository repository)
174   {
175     final String webUser = config.getWebUser();
176     final String webPassword = config.getWebPassword();
177 
178     if (StringUtils.isNotBlank(webUser) && StringUtils.isNotBlank(webPassword))
179     {
180       if (log.isTraceEnabled())
181       {
182         log.trace("Setting web credentials '" + webUser + "' with password '"
183                   + StringUtils.repeat("*", webPassword.length()) + "'.");
184       }
185       final AuthenticationCredentials credentials =
186           new AuthenticationCredentials(webUser, webPassword);
187       repository.setCredentials(AuthenticationType.HTTP, credentials, false);
188     }
189 
190     final String issueManagementUser = config.getIssueManagementUser();
191     final String issueManagementPassword = config.getIssueManagementPassword();
192     if (StringUtils.isNotBlank(issueManagementUser)
193         && StringUtils.isNotBlank(issueManagementPassword))
194     {
195       if (log.isTraceEnabled())
196       {
197         log.trace("Setting repository credentials '" + issueManagementUser
198                   + "' with password '"
199                   + StringUtils.repeat("*", issueManagementPassword.length())
200                   + "'.");
201       }
202       final AuthenticationCredentials credentials =
203           new AuthenticationCredentials(issueManagementUser,
204               issueManagementPassword);
205 
206       repository.setCredentials(AuthenticationType.REPOSITORY, credentials,
207           false);
208     }
209   }
210 
211   /**
212    * Simple method to sleep a given period of time before a connection retry.
213    * The timeout in milliseconds is defined in {@link #config}.
214    */
215   private void sleep()
216   {
217     try
218     {
219       Thread.sleep(config.getTimeout());
220     }
221     catch (final InterruptedException e)
222     {
223       // just continue;
224     }
225   }
226 
227   // --- get&set --------------------------------------------------------------
228 
229   /**
230    * {@inheritDoc}
231    */
232   public AbstractRepositoryConnector getConnector()
233   {
234     return connector;
235   }
236 
237   // --- business -------------------------------------------------------------
238 
239   /**
240    * {@inheritDoc}
241    *
242    * @see de.smartics.maven.issues.RepositoryFacade#queryTasks(QueryData)
243    */
244   public List<TaskData> queryTasks(final QueryData queryData)
245     throws MavenReportException
246   {
247     final SimpleTaskDataCollector collector = new SimpleTaskDataCollector();
248     runQuery(queryData, collector);
249     return fetchTaskData(collector);
250   }
251 
252   /**
253    * Runs the query for matching tasks. The result is contained in the hit
254    * collector.
255    *
256    * @param queryData the information to construct a query against the remote
257    *          issue management system.
258    * @param collector the collector to run the query and contain the hits.
259    * @throws MavenReportException if a problem is encountered while fetching the
260    *           tasks from the remote repository.
261    */
262   private void runQuery(final QueryData queryData,
263       final SimpleTaskDataCollector collector) throws MavenReportException
264   {
265     if (log.isDebugEnabled())
266     {
267       log.debug("Searching for matching bugs...");
268     }
269 
270     final int maxRetries = config.getMaxRetries();
271     final boolean ignoreLogoutProblem = config.isIgnoreLogoutProblem();
272     final IRepositoryQuery query = constructQuery(queryData);
273 
274     if (log.isDebugEnabled())
275     {
276       log.debug("Connecting to " + query.getUrl() + " (max retries "
277                 + maxRetries + ")...");
278     }
279     final IProgressMonitor monitor = new NullProgressMonitor();
280     for (int tryNumber = 0; tryNumber <= maxRetries; tryNumber++)
281     {
282       final IStatus status =
283           connector.performQuery(repository, query, collector, createSession(),
284               monitor);
285 
286       if (log.isTraceEnabled())
287       {
288         log.trace("Query performed with status '" + status + "'.");
289       }
290 
291       if (shouldBreak(status, ignoreLogoutProblem, maxRetries, tryNumber))
292       {
293         checkException(query, status);
294         break;
295       }
296     }
297 
298     if (log.isTraceEnabled())
299     {
300       log.trace("Collected " + collector.getTasks().size() + " issues.");
301     }
302 
303   }
304 
305   /**
306    * Checks if an exception should be thrown.
307    *
308    * @param query the query that was running.
309    * @param status the status returned from running the query.
310    * @throws MavenReportException if the status signals that this exception is
311    *           required to be raised.
312    */
313   private void checkException(final IRepositoryQuery query, final IStatus status)
314     throws MavenReportException
315   {
316     if (status.getSeverity() == IStatus.ERROR)
317     {
318       final MavenReportException e =
319           new MavenReportException("Problem connecting to '" + query.getUrl()
320                                    + "': " + status.getMessage());
321       e.initCause(status.getException());
322       throw e;
323     }
324   }
325 
326   /**
327    * We are called from within a loop making retries on connection problems.
328    * This method checks if we should continue with the next loop iteration or
329    * not.
330    *
331    * @param status the status returned by the query from the issue management
332    *          system.
333    * @param ignoreLogoutProblem <code>true</code> if logout problems should be
334    *          ignored in which case we will break in case of a logout problem.
335    *          <code>false</code> if logout problems should be treated as any
336    *          other problems where we wait a short period and retry, returning
337    *          <code>false</code> as to not break.
338    * @param maxRetries the number of maximum retries.
339    * @param tryNumber the current try.
340    * @return <code>true</code> if we should stop iterating, <code>false</code>
341    *         if we should give the issue management system another try.
342    * @throws MavenReportException if the maximum number of tries is exceeded and
343    *           the last error message returned from the issue management system
344    *           is to be returned.
345    */
346   private boolean shouldBreak(final IStatus status,
347       final boolean ignoreLogoutProblem, final int maxRetries,
348       final int tryNumber) throws MavenReportException
349   {
350     if (status.getSeverity() == IStatus.ERROR)
351     {
352       final boolean logoutProblem = isLogoutProblem(status);
353       if (!logoutProblem || !ignoreLogoutProblem)
354       {
355         retry(status, maxRetries, tryNumber);
356         return false;
357       }
358     }
359     return true;
360   }
361 
362   /**
363    * Checks if the status signals a logout problem. Logout problems may be
364    * treated differently depending on the configuration. Subclasses are
365    * responsible to check if the status is reporting a logout problem or not.
366    *
367    * @param status the status returned from the issue management system.
368    * @return <code>true</code> if this is a logout problem, <code>false</code>
369    *         otherwise. If unsure, subclasses should return <code>false</code>.
370    */
371   protected abstract boolean isLogoutProblem(IStatus status);
372 
373   /**
374    * Retry if <code>maxRetries</code> is not exceeded after a short break
375    * (sleep).
376    *
377    * @param status the status returned by the query from the issue management
378    *          system.
379    * @param maxRetries the number of maximum retries.
380    * @param tryNumber the current try.
381    * @throws MavenReportException if the maximum number of tries is exceeded.
382    */
383   private void retry(final IStatus status, final int maxRetries,
384       final int tryNumber) throws MavenReportException
385   {
386     final String statusMessage = status.getMessage();
387     if (tryNumber == maxRetries)
388     {
389       throw new MavenReportException(statusMessage,
390           (Exception) status.getException());
391     }
392     else
393     {
394       if (log.isWarnEnabled())
395       {
396         log.warn("Connection (retry # " + tryNumber + "): " + statusMessage);
397       }
398       sleep();
399     }
400   }
401 
402   /**
403    * Returns a session.
404    *
405    * @return <code>null</code> since sessions are not necessary to run the
406    *         query, but may be relevant to subclasses.
407    */
408   protected ISynchronizationSession createSession()
409   {
410     return null;
411   }
412 
413   /**
414    * Upon the hits provided by the collector we fetch additional information we
415    * are interested in for each task.
416    *
417    * @param hitCollector the collector with hits.
418    * @return the task information to render in the report.
419    * @throws MavenReportException if a problem is encountered while fetching the
420    *           tasks from the remote repository.
421    */
422   private List<TaskData> fetchTaskData(
423       final SimpleTaskDataCollector hitCollector) throws MavenReportException
424   {
425     final TaskDataCache taskDataCache =
426         ResourceLocator.getInstance().getTaskDataCache();
427 
428     final boolean logColumns = config.isLogColumns();
429     final Set<String> columnIds = logColumns ? new HashSet<String>() : null;
430     final int maxEntries = config.getMaxEntries();
431     final int maxRetries = config.getMaxRetries();
432     final List<TaskData> issues = new ArrayList<TaskData>(128);
433     try
434     {
435       int taskCounter = 0;
436       for (TaskData minimallyFilledTask : hitCollector.getTasks())
437       {
438         if (isQuit(maxEntries, taskCounter))
439         {
440           logQuitting(maxEntries);
441           break;
442         }
443 
444         final String taskId = minimallyFilledTask.getTaskId();
445         logIssueId(taskId);
446 
447         final TaskData completeTaskData =
448             fetchTaskData(taskDataCache, minimallyFilledTask, maxRetries);
449         if (completeTaskData != null)
450         {
451           addLogColumns(columnIds, completeTaskData);
452           issues.add(completeTaskData);
453           taskCounter++;
454         }
455       }
456     }
457     catch (CoreException e)
458     {
459       throw new MavenReportException(e.getMessage(), e);
460     }
461 
462     if (logColumns)
463     {
464       log.info("Attribute IDs: " + new TreeSet<String>(columnIds));
465     }
466 
467     return issues;
468   }
469 
470   /**
471    * Logs the issue ID at trace level.
472    *
473    * @param taskId the task ID to log.
474    */
475   private void logIssueId(final String taskId)
476   {
477     if (log.isTraceEnabled())
478     {
479       log.trace("Retrieving issue: " + taskId);
480     }
481   }
482 
483   /**
484    * Logs that the query is quitting.
485    *
486    * @param maxEntries the max entries that have been fetched.
487    */
488   private void logQuitting(final int maxEntries)
489   {
490     if (log.isDebugEnabled())
491     {
492       log.debug("Quitting fetching tasks after task " + maxEntries + '.');
493     }
494   }
495 
496   /**
497    * Adds the task attribute IDs (used to log columns hence the name of the
498    * method) to the given set.
499    *
500    * @param columnIds the set of IDs to add the new ones to. If value is
501    *          <code>null</code> nothing is logged.
502    * @param taskData the task whose attributes are to be logged.
503    */
504   private void addLogColumns(final Set<String> columnIds,
505       final TaskData taskData)
506   {
507     if (columnIds != null)
508     {
509       final TaskAttribute root = taskData.getRoot();
510       final Map<String, TaskAttribute> attributes = root.getAttributes();
511       final Set<String> attributeIds = attributes.keySet();
512       columnIds.addAll(attributeIds);
513     }
514   }
515 
516   /**
517    * Checks if max entries have been read and reading should quit.
518    *
519    * @param maxEntries the maximum number of issues to be read.
520    * @param taskCounter the current number of issues already read.
521    * @return <code>true</code> if reading should quit, false if reading should
522    *         continue.
523    */
524   private static boolean isQuit(final int maxEntries, final int taskCounter)
525   {
526     return maxEntries != -1 && taskCounter >= maxEntries;
527   }
528 
529   /**
530    * Fetches the completely filled task for the minimally filled task.
531    *
532    * @param taskDataCache the cache to retrieve task information from.
533    * @param minimallyFilledTask the task for which the complete information is
534    *          requested.
535    * @param maxRetries the maximum retries to launch on connection problems
536    *          before giving up.
537    * @return the completely filled task.
538    * @throws CoreException if the task cannot be fetched.
539    */
540   private TaskData fetchTaskData(final TaskDataCache taskDataCache,
541       final TaskData minimallyFilledTask, final int maxRetries)
542     throws CoreException
543   {
544     for (int tryNumber = 0; tryNumber <= maxRetries; tryNumber++)
545     {
546       try
547       {
548         final TaskData completeTaskData =
549             readAndCacheTaskData(taskDataCache, minimallyFilledTask);
550         return completeTaskData;
551       }
552       catch (final CoreException e)
553       {
554         if (tryNumber == maxRetries)
555         {
556           throw e;
557         }
558         else
559         {
560           if (log.isWarnEnabled())
561           {
562             log.warn("Connection (retry # " + tryNumber + "): "
563                      + e.getMessage());
564           }
565           sleep();
566         }
567       }
568     }
569     if (log.isWarnEnabled())
570     {
571       log.warn("No information for task " + minimallyFilledTask.getTaskId()
572                + "' can be retrieved. Omitting this bug from the report.");
573     }
574     return null;
575   }
576 
577   /**
578    * Fetches the bug from the cache and, on a cache-miss, requests the bug
579    * information from the remote issue management system.
580    *
581    * @param taskDataCache the cache to fetch bugs from and store new bugs in.
582    * @param task the task to fetch additional task data.
583    * @return the task data for the given task.
584    * @throws CoreException if the task cannot be fetched.
585    */
586   private TaskData readAndCacheTaskData(final TaskDataCache taskDataCache,
587       final TaskData task) throws CoreException
588   {
589     final String id = task.getTaskId();
590     TaskData taskData = taskDataCache.getTask(id);
591     if (taskData == null)
592     {
593       taskData =
594           connector.getTaskData(repository, id, new NullProgressMonitor());
595       taskDataCache.addTask(taskData);
596       if (log.isTraceEnabled())
597       {
598         log.trace("  Cache miss for bug " + id + '.');
599       }
600     }
601     return taskData;
602   }
603 
604   /**
605    * Constructs the query upon the <code>queryData</code>. Since the query is
606    * dependent on the issue management system in use, this method is to be
607    * implemented by subclasses.
608    *
609    * @param queryData the issue management independent representation of the
610    *          query.
611    * @return the query to execute to fetch the specified task data.
612    * @throws MavenReportException if the query data is invalid to create a issue
613    *           management specific query.
614    */
615   protected abstract IRepositoryQuery constructQuery(QueryData queryData)
616     throws MavenReportException;
617 
618   // --- object basics --------------------------------------------------------
619 
620 }