View Javadoc

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