Merge remote-tracking branch 'origin/master' into mikhail.golubev/configurable-issues...
[idea/community.git] / plugins / tasks / tasks-core / src / com / intellij / tasks / impl / TaskManagerImpl.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
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 com.intellij.tasks.impl;
17
18 import com.intellij.notification.*;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.components.*;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.options.ShowSettingsUtil;
23 import com.intellij.openapi.progress.EmptyProgressIndicator;
24 import com.intellij.openapi.progress.ProcessCanceledException;
25 import com.intellij.openapi.progress.ProgressIndicator;
26 import com.intellij.openapi.progress.ProgressManager;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.startup.StartupManager;
29 import com.intellij.openapi.ui.Messages;
30 import com.intellij.openapi.util.Comparing;
31 import com.intellij.openapi.util.Condition;
32 import com.intellij.openapi.util.text.StringUtil;
33 import com.intellij.openapi.vcs.AbstractVcs;
34 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
35 import com.intellij.openapi.vcs.VcsTaskHandler;
36 import com.intellij.openapi.vcs.VcsType;
37 import com.intellij.openapi.vcs.changes.*;
38 import com.intellij.tasks.*;
39 import com.intellij.tasks.actions.TaskSearchSupport;
40 import com.intellij.tasks.config.TaskRepositoriesConfigurable;
41 import com.intellij.tasks.context.WorkingContextManager;
42 import com.intellij.ui.ColoredTreeCellRenderer;
43 import com.intellij.util.ArrayUtil;
44 import com.intellij.util.EventDispatcher;
45 import com.intellij.util.Function;
46 import com.intellij.util.containers.ContainerUtil;
47 import com.intellij.util.containers.Convertor;
48 import com.intellij.util.containers.MultiMap;
49 import com.intellij.util.ui.UIUtil;
50 import com.intellij.util.xmlb.XmlSerializationException;
51 import com.intellij.util.xmlb.XmlSerializer;
52 import com.intellij.util.xmlb.XmlSerializerUtil;
53 import com.intellij.util.xmlb.annotations.AbstractCollection;
54 import com.intellij.util.xmlb.annotations.Property;
55 import com.intellij.util.xmlb.annotations.Tag;
56 import org.jdom.Element;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59 import org.jetbrains.annotations.TestOnly;
60
61 import javax.swing.Timer;
62 import javax.swing.event.HyperlinkEvent;
63 import java.awt.event.ActionEvent;
64 import java.awt.event.ActionListener;
65 import java.net.SocketTimeoutException;
66 import java.net.UnknownHostException;
67 import java.text.DecimalFormat;
68 import java.util.*;
69 import java.util.concurrent.Future;
70 import java.util.concurrent.TimeUnit;
71 import java.util.concurrent.TimeoutException;
72
73
74 /**
75  * @author Dmitry Avdeev
76  */
77 @State(
78   name = "TaskManager",
79   storages = {
80     @Storage(file = StoragePathMacros.WORKSPACE_FILE)
81   }
82 )
83 public class TaskManagerImpl extends TaskManager implements ProjectComponent, PersistentStateComponent<TaskManagerImpl.Config>,
84                                                             ChangeListDecorator {
85
86   private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.impl.TaskManagerImpl");
87
88   private static final DecimalFormat LOCAL_TASK_ID_FORMAT = new DecimalFormat("LOCAL-00000");
89   public static final Comparator<Task> TASK_UPDATE_COMPARATOR = new Comparator<Task>() {
90     public int compare(@NotNull Task o1, @NotNull Task o2) {
91       int i = Comparing.compare(o2.getUpdated(), o1.getUpdated());
92       return i == 0 ? Comparing.compare(o2.getCreated(), o1.getCreated()) : i;
93     }
94   };
95   private static final Convertor<Task, String> KEY_CONVERTOR = new Convertor<Task, String>() {
96     @Override
97     public String convert(Task o) {
98       return o.getId();
99     }
100   };
101   static final String TASKS_NOTIFICATION_GROUP = "Task Group";
102
103   private final Project myProject;
104
105   private final WorkingContextManager myContextManager;
106
107   private final Map<String, Task> myIssueCache = Collections.synchronizedMap(new LinkedHashMap<String, Task>());
108
109   private final Map<String, LocalTask> myTasks = Collections.synchronizedMap(new LinkedHashMap<String, LocalTask>() {
110     @Override
111     public LocalTask put(String key, LocalTask task) {
112       LocalTask result = super.put(key, task);
113       if (size() > myConfig.taskHistoryLength) {
114         ArrayList<Map.Entry<String, LocalTask>> list = new ArrayList<Map.Entry<String,LocalTask>>(entrySet());
115         Collections.sort(list, new Comparator<Map.Entry<String, LocalTask>>() {
116           @Override
117           public int compare(@NotNull Map.Entry<String, LocalTask> o1, @NotNull Map.Entry<String, LocalTask> o2) {
118             return TASK_UPDATE_COMPARATOR.compare(o2.getValue(), o1.getValue());
119           }
120         });
121         for (Map.Entry<String, LocalTask> oldest : list) {
122           if (!oldest.getValue().isDefault()) {
123             remove(oldest.getKey());
124             break;
125           }
126         }
127       }
128       return result;
129     }
130   });
131
132   @NotNull
133   private LocalTask myActiveTask = createDefaultTask();
134   private Timer myCacheRefreshTimer;
135
136   private volatile boolean myUpdating;
137   private final Config myConfig = new Config();
138   private final ChangeListAdapter myChangeListListener;
139   private final ChangeListManager myChangeListManager;
140
141   private final List<TaskRepository> myRepositories = new ArrayList<TaskRepository>();
142   private final EventDispatcher<TaskListener> myDispatcher = EventDispatcher.create(TaskListener.class);
143   private Set<TaskRepository> myBadRepositories = ContainerUtil.newConcurrentSet();
144
145   public TaskManagerImpl(Project project, WorkingContextManager contextManager, ChangeListManager changeListManager) {
146
147     myProject = project;
148     myContextManager = contextManager;
149     myChangeListManager = changeListManager;
150
151     myChangeListListener = new ChangeListAdapter() {
152       @Override
153       public void changeListRemoved(ChangeList list) {
154         LocalTask task = getAssociatedTask((LocalChangeList)list);
155         if (task != null) {
156           for (ChangeListInfo info : task.getChangeLists()) {
157             if (Comparing.equal(info.id, ((LocalChangeList)list).getId())) {
158               info.id = "";
159             }
160           }
161         }
162       }
163
164       @Override
165       public void defaultListChanged(ChangeList oldDefaultList, ChangeList newDefaultList) {
166         final LocalTask associatedTask = getAssociatedTask((LocalChangeList)newDefaultList);
167         if (associatedTask != null && !getActiveTask().equals(associatedTask)) {
168           ApplicationManager.getApplication().invokeLater(new Runnable() {
169             public void run() {
170               activateTask(associatedTask, true);
171             }
172           }, myProject.getDisposed());
173         }
174       }
175     };
176   }
177
178   @Override
179   public TaskRepository[] getAllRepositories() {
180     return myRepositories.toArray(new TaskRepository[myRepositories.size()]);
181   }
182
183   public <T extends TaskRepository> void setRepositories(List<T> repositories) {
184
185     Set<TaskRepository> set = new HashSet<TaskRepository>(myRepositories);
186     set.removeAll(repositories);
187     myBadRepositories.removeAll(set); // remove all changed reps
188     myIssueCache.clear();
189
190     myRepositories.clear();
191     myRepositories.addAll(repositories);
192
193     reps:
194     for (T repository : repositories) {
195       if (repository.isShared() && repository.getUrl() != null) {
196         List<TaskProjectConfiguration.SharedServer> servers = getProjectConfiguration().servers;
197         TaskRepositoryType type = repository.getRepositoryType();
198         for (TaskProjectConfiguration.SharedServer server : servers) {
199           if (repository.getUrl().equals(server.url) && type.getName().equals(server.type)) {
200             continue reps;
201           }
202         }
203         TaskProjectConfiguration.SharedServer server = new TaskProjectConfiguration.SharedServer();
204         server.type = type.getName();
205         server.url = repository.getUrl();
206         servers.add(server);
207       }
208     }
209   }
210
211   @Override
212   public void removeTask(LocalTask task) {
213     if (task.isDefault()) return;
214     if (myActiveTask.equals(task)) {
215       activateTask(myTasks.get(LocalTaskImpl.DEFAULT_TASK_ID), true);
216     }
217     myTasks.remove(task.getId());
218     myDispatcher.getMulticaster().taskRemoved(task);
219     myContextManager.removeContext(task);
220   }
221
222   @Override
223   public void addTaskListener(TaskListener listener) {
224     myDispatcher.addListener(listener);
225   }
226
227   @Override
228   public void removeTaskListener(TaskListener listener) {
229     myDispatcher.removeListener(listener);
230   }
231
232   @NotNull
233   @Override
234   public LocalTask getActiveTask() {
235     return myActiveTask;
236   }
237
238   @Nullable
239   @Override
240   public LocalTask findTask(String id) {
241     return myTasks.get(id);
242   }
243
244   @NotNull
245   @Override
246   public List<Task> getIssues(@Nullable final String query) {
247     return getIssues(query, true);
248   }
249
250   @Override
251   public List<Task> getIssues(@Nullable final String query, final boolean forceRequest) {
252     return getIssues(query, 0, 50, true, new EmptyProgressIndicator(), forceRequest);
253   }
254
255   @Override
256   public List<Task> getIssues(@Nullable String query,
257                               int offset,
258                               int limit,
259                               final boolean withClosed,
260                               @NotNull ProgressIndicator indicator,
261                               boolean forceRequest) {
262     List<Task> tasks = getIssuesFromRepositories(query, offset, limit, withClosed, forceRequest, indicator);
263     if (tasks == null) {
264       return getCachedIssues(withClosed);
265     }
266     myIssueCache.putAll(ContainerUtil.newMapFromValues(tasks.iterator(), KEY_CONVERTOR));
267     return ContainerUtil.filter(tasks, new Condition<Task>() {
268       @Override
269       public boolean value(final Task task) {
270         return withClosed || !task.isClosed();
271       }
272     });
273   }
274
275   @Override
276   public List<Task> getCachedIssues() {
277     return getCachedIssues(true);
278   }
279
280   @Override
281   public List<Task> getCachedIssues(final boolean withClosed) {
282     return ContainerUtil.filter(myIssueCache.values(), new Condition<Task>() {
283       @Override
284       public boolean value(final Task task) {
285         return withClosed || !task.isClosed();
286       }
287     });
288   }
289
290   @Nullable
291   @Override
292   public Task updateIssue(@NotNull String id) {
293     for (TaskRepository repository : getAllRepositories()) {
294       if (repository.extractId(id) == null) {
295         continue;
296       }
297       try {
298         Task issue = repository.findTask(id);
299         if (issue != null) {
300           LocalTask localTask = findTask(id);
301           if (localTask != null) {
302             localTask.updateFromIssue(issue);
303             return localTask;
304           }
305           return issue;
306         }
307       }
308       catch (Exception e) {
309         LOG.info(e);
310       }
311     }
312     return null;
313   }
314
315   @Override
316   public List<LocalTask> getLocalTasks() {
317     return getLocalTasks(true);
318   }
319
320   @Override
321   public List<LocalTask> getLocalTasks(final boolean withClosed) {
322     synchronized (myTasks) {
323       return ContainerUtil.filter(myTasks.values(), new Condition<LocalTask>() {
324         @Override
325         public boolean value(final LocalTask task) {
326           return withClosed || !isLocallyClosed(task);
327         }
328       });
329     }
330   }
331
332   @Override
333   public LocalTask addTask(Task issue) {
334     LocalTaskImpl task = issue instanceof LocalTaskImpl ? (LocalTaskImpl)issue : new LocalTaskImpl(issue);
335     addTask(task);
336     return task;
337   }
338
339   @Override
340   public LocalTaskImpl createLocalTask(@NotNull String summary) {
341     return createTask(LOCAL_TASK_ID_FORMAT.format(myConfig.localTasksCounter++), summary);
342   }
343
344   private static LocalTaskImpl createTask(@NotNull String id, @NotNull String summary) {
345     LocalTaskImpl task = new LocalTaskImpl(id, summary);
346     Date date = new Date();
347     task.setCreated(date);
348     task.setUpdated(date);
349     return task;
350   }
351
352   @Override
353   public LocalTask activateTask(@NotNull final Task origin, boolean clearContext) {
354     LocalTask activeTask = getActiveTask();
355     if (origin.equals(activeTask)) return activeTask;
356
357     saveActiveTask();
358
359     if (clearContext) {
360       myContextManager.clearContext();
361     }
362     myContextManager.restoreContext(origin);
363
364     final LocalTask task = doActivate(origin, true);
365
366     return restoreVcsContext(task);
367   }
368
369   private LocalTask restoreVcsContext(LocalTask task) {
370     if (!isVcsEnabled()) return task;
371
372     List<ChangeListInfo> changeLists = task.getChangeLists();
373     if (!changeLists.isEmpty()) {
374       ChangeListInfo info = changeLists.get(0);
375       LocalChangeList changeList = myChangeListManager.getChangeList(info.id);
376       if (changeList == null) {
377         changeList = myChangeListManager.addChangeList(info.name, info.comment);
378         info.id = changeList.getId();
379       }
380       myChangeListManager.setDefaultChangeList(changeList);
381     }
382
383     List<BranchInfo> branches = task.getBranches(false);
384     // we should have exactly one branch per repo
385     MultiMap<String, BranchInfo> multiMap = new MultiMap<String, BranchInfo>();
386     for (BranchInfo branch : branches) {
387       multiMap.putValue(branch.repository, branch);
388     }
389     for (String repo: multiMap.keySet()) {
390       Collection<BranchInfo> infos = multiMap.get(repo);
391       if (infos.size() > 1) {
392         // cleanup needed
393         List<BranchInfo> existing = getAllBranches(repo);
394         for (Iterator<BranchInfo> iterator = infos.iterator(); iterator.hasNext(); ) {
395           BranchInfo info = iterator.next();
396           if (!existing.contains(info)) {
397             iterator.remove();
398             if (infos.size() == 1) {
399               break;
400             }
401           }
402         }
403       }
404     }
405
406     VcsTaskHandler.TaskInfo info = fromBranches(new ArrayList<BranchInfo>(multiMap.values()));
407
408     switchBranch(info);
409     return task;
410   }
411
412   private List<BranchInfo> getAllBranches(final String repo) {
413     ArrayList<BranchInfo> infos = new ArrayList<BranchInfo>();
414     VcsTaskHandler[] handlers = VcsTaskHandler.getAllHandlers(myProject);
415     for (VcsTaskHandler handler : handlers) {
416       VcsTaskHandler.TaskInfo[] tasks = handler.getAllExistingTasks();
417       for (VcsTaskHandler.TaskInfo info : tasks) {
418         infos.addAll(ContainerUtil.filter(BranchInfo.fromTaskInfo(info, false), new Condition<BranchInfo>() {
419           @Override
420           public boolean value(BranchInfo info) {
421             return Comparing.equal(info.repository, repo);
422           }
423         }));
424       }
425     }
426     return infos;
427   }
428
429   private void switchBranch(VcsTaskHandler.TaskInfo info) {
430     VcsTaskHandler[] handlers = VcsTaskHandler.getAllHandlers(myProject);
431     for (VcsTaskHandler handler : handlers) {
432       handler.switchToTask(info, null);
433     }
434   }
435
436   private static VcsTaskHandler.TaskInfo fromBranches(List<BranchInfo> branches) {
437     if (branches.isEmpty()) return new VcsTaskHandler.TaskInfo(null, Collections.<String>emptyList());
438     MultiMap<String, String> map = new MultiMap<String, String>();
439     for (BranchInfo branch : branches) {
440       map.putValue(branch.name, branch.repository);
441     }
442     Map.Entry<String, Collection<String>> next = map.entrySet().iterator().next();
443     return new VcsTaskHandler.TaskInfo(next.getKey(), next.getValue());
444   }
445
446   public void createBranch(LocalTask task, LocalTask previousActive, String name) {
447     VcsTaskHandler[] handlers = VcsTaskHandler.getAllHandlers(myProject);
448     for (VcsTaskHandler handler : handlers) {
449       VcsTaskHandler.TaskInfo[] info = handler.getCurrentTasks();
450       if (previousActive != null && previousActive.getBranches(false).isEmpty()) {
451         addBranches(previousActive, info, false);
452       }
453       addBranches(task, info, true);
454       addBranches(task, new VcsTaskHandler.TaskInfo[] { handler.startNewTask(name) }, false);
455     }
456   }
457
458   public void mergeBranch(LocalTask task) {
459     VcsTaskHandler.TaskInfo original = fromBranches(task.getBranches(true));
460     VcsTaskHandler.TaskInfo feature = fromBranches(task.getBranches(false));
461
462     VcsTaskHandler[] handlers = VcsTaskHandler.getAllHandlers(myProject);
463     for (VcsTaskHandler handler : handlers) {
464       handler.closeTask(feature, original);
465     }
466   }
467
468   private static void addBranches(LocalTask task, VcsTaskHandler.TaskInfo[] info, boolean original) {
469     for (VcsTaskHandler.TaskInfo taskInfo : info) {
470       List<BranchInfo> branchInfos = BranchInfo.fromTaskInfo(taskInfo, original);
471       for (BranchInfo branchInfo : branchInfos) {
472         task.addBranch(branchInfo);
473       }
474     }
475   }
476
477   private void saveActiveTask() {
478     myContextManager.saveContext(myActiveTask);
479     myActiveTask.setUpdated(new Date());
480   }
481
482   private LocalTask doActivate(Task origin, boolean explicitly) {
483     final LocalTaskImpl task = origin instanceof LocalTaskImpl ? (LocalTaskImpl)origin : new LocalTaskImpl(origin);
484     if (explicitly) {
485       task.setUpdated(new Date());
486     }
487     myActiveTask.setActive(false);
488     task.setActive(true);
489     addTask(task);
490     if (task.isIssue()) {
491       StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
492         public void run() {
493           ProgressManager.getInstance().run(new com.intellij.openapi.progress.Task.Backgroundable(myProject, "Updating " + task.getId()) {
494
495             public void run(@NotNull ProgressIndicator indicator) {
496               updateIssue(task.getId());
497             }
498           });
499         }
500       });
501     }
502     LocalTask oldActiveTask = myActiveTask;
503     boolean isChanged = !task.equals(oldActiveTask);
504     myActiveTask = task;
505     if (isChanged) {
506       myDispatcher.getMulticaster().taskDeactivated(oldActiveTask);
507       myDispatcher.getMulticaster().taskActivated(task);
508     }
509     return task;
510   }
511
512   private void addTask(LocalTaskImpl task) {
513     myTasks.put(task.getId(), task);
514     myDispatcher.getMulticaster().taskAdded(task);
515   }
516
517   @Override
518   public boolean testConnection(final TaskRepository repository) {
519
520     TestConnectionTask task = new TestConnectionTask("Test connection") {
521       public void run(@NotNull ProgressIndicator indicator) {
522         indicator.setText("Connecting to " + repository.getUrl() + "...");
523         indicator.setFraction(0);
524         indicator.setIndeterminate(true);
525         try {
526           myConnection = repository.createCancellableConnection();
527           if (myConnection != null) {
528             Future<Exception> future = ApplicationManager.getApplication().executeOnPooledThread(myConnection);
529             while (true) {
530               try {
531                 myException = future.get(100, TimeUnit.MILLISECONDS);
532                 return;
533               }
534               catch (TimeoutException ignore) {
535                 try {
536                   indicator.checkCanceled();
537                 }
538                 catch (ProcessCanceledException e) {
539                   myException = e;
540                   myConnection.cancel();
541                   return;
542                 }
543               }
544               catch (Exception e) {
545                 myException = e;
546                 return;
547               }
548             }
549           }
550           else {
551             try {
552               repository.testConnection();
553             }
554             catch (Exception e) {
555               LOG.info(e);
556               myException = e;
557             }
558           }
559         }
560         catch (Exception e) {
561           myException = e;
562         }
563       }
564     };
565     ProgressManager.getInstance().run(task);
566     Exception e = task.myException;
567     if (e == null) {
568       myBadRepositories.remove(repository);
569       Messages.showMessageDialog(myProject, "Connection is successful", "Connection", Messages.getInformationIcon());
570     }
571     else if (!(e instanceof ProcessCanceledException)) {
572       String message = e.getMessage();
573       if (e instanceof UnknownHostException) {
574         message = "Unknown host: " + message;
575       }
576       if (message == null) {
577         LOG.error(e);
578         message = "Unknown error";
579       }
580       Messages.showErrorDialog(myProject, StringUtil.capitalize(message), "Error");
581     }
582     return e == null;
583   }
584
585   @NotNull
586   public Config getState() {
587     myConfig.tasks = ContainerUtil.map(myTasks.values(), new Function<Task, LocalTaskImpl>() {
588       public LocalTaskImpl fun(Task task) {
589         return new LocalTaskImpl(task);
590       }
591     });
592     myConfig.servers = XmlSerializer.serialize(getAllRepositories());
593     return myConfig;
594   }
595
596   public void loadState(Config config) {
597     XmlSerializerUtil.copyBean(config, myConfig);
598     myTasks.clear();
599     for (LocalTaskImpl task : config.tasks) {
600       addTask(task);
601     }
602
603     myRepositories.clear();
604     Element element = config.servers;
605     List<TaskRepository> repositories = loadRepositories(element);
606     myRepositories.addAll(repositories);
607   }
608
609   public static ArrayList<TaskRepository> loadRepositories(Element element) {
610     ArrayList<TaskRepository> repositories = new ArrayList<TaskRepository>();
611     for (TaskRepositoryType repositoryType : TaskRepositoryType.getRepositoryTypes()) {
612       for (Object o : element.getChildren()) {
613         if (((Element)o).getName().equals(repositoryType.getName())) {
614           try {
615             @SuppressWarnings({"unchecked"})
616             TaskRepository repository = (TaskRepository)XmlSerializer.deserialize((Element)o, repositoryType.getRepositoryClass());
617             if (repository != null) {
618               repository.setRepositoryType(repositoryType);
619               repositories.add(repository);
620             }
621           }
622           catch (XmlSerializationException e) {
623             LOG.error(e.getMessage(), e);
624           }
625         }
626       }
627     }
628     return repositories;
629   }
630
631   public void projectOpened() {
632
633     TaskProjectConfiguration projectConfiguration = getProjectConfiguration();
634
635     servers:
636     for (TaskProjectConfiguration.SharedServer server : projectConfiguration.servers) {
637       if (server.type == null || server.url == null) {
638         continue;
639       }
640       for (TaskRepositoryType<?> repositoryType : TaskRepositoryType.getRepositoryTypes()) {
641         if (repositoryType.getName().equals(server.type)) {
642           for (TaskRepository repository : myRepositories) {
643             if (!repositoryType.equals(repository.getRepositoryType())) {
644               continue;
645             }
646             if (server.url.equals(repository.getUrl())) {
647               continue servers;
648             }
649           }
650           TaskRepository repository = repositoryType.createRepository();
651           repository.setUrl(server.url);
652           myRepositories.add(repository);
653         }
654       }
655     }
656
657     myContextManager.pack(200, 50);
658
659     // make sure the task is associated with default changelist
660     LocalTask defaultTask = findTask(LocalTaskImpl.DEFAULT_TASK_ID);
661     LocalChangeList defaultList = myChangeListManager.findChangeList(LocalChangeList.DEFAULT_NAME);
662     if (defaultList != null && defaultTask != null) {
663       ChangeListInfo listInfo = new ChangeListInfo(defaultList);
664       if (!defaultTask.getChangeLists().contains(listInfo)) {
665         defaultTask.addChangelist(listInfo);
666       }
667     }
668
669     // remove already not existing changelists from tasks changelists
670     for (LocalTask localTask : getLocalTasks()) {
671       for (Iterator<ChangeListInfo> iterator = localTask.getChangeLists().iterator(); iterator.hasNext(); ) {
672         final ChangeListInfo changeListInfo = iterator.next();
673         if (myChangeListManager.getChangeList(changeListInfo.id) == null) {
674           iterator.remove();
675         }
676       }
677     }
678
679     myChangeListManager.addChangeListListener(myChangeListListener);
680   }
681
682   private TaskProjectConfiguration getProjectConfiguration() {
683     return ServiceManager.getService(myProject, TaskProjectConfiguration.class);
684   }
685
686   public void projectClosed() {
687   }
688
689   @NotNull
690   public String getComponentName() {
691     return "Task Manager";
692   }
693
694   public void initComponent() {
695     if (!ApplicationManager.getApplication().isUnitTestMode()) {
696       myCacheRefreshTimer = UIUtil.createNamedTimer("TaskManager refresh", myConfig.updateInterval * 60 * 1000, new ActionListener() {
697         public void actionPerformed(@NotNull ActionEvent e) {
698           if (myConfig.updateEnabled && !myUpdating) {
699             updateIssues(null);
700           }
701         }
702       });
703       myCacheRefreshTimer.setInitialDelay(0);
704       StartupManager.getInstance(myProject).registerPostStartupActivity(new Runnable() {
705         public void run() {
706           myCacheRefreshTimer.start();
707         }
708       });
709     }
710
711     // make sure that the default task is exist
712     LocalTask defaultTask = findTask(LocalTaskImpl.DEFAULT_TASK_ID);
713     if (defaultTask == null) {
714       defaultTask = createDefaultTask();
715       addTask(defaultTask);
716     }
717
718     // search for active task
719     LocalTask activeTask = null;
720     final List<LocalTask> tasks = getLocalTasks();
721     Collections.sort(tasks, TASK_UPDATE_COMPARATOR);
722     for (LocalTask task : tasks) {
723       if (activeTask == null) {
724         if (task.isActive()) {
725           activeTask = task;
726         }
727       }
728       else {
729         task.setActive(false);
730       }
731     }
732     if (activeTask == null) {
733       activeTask = defaultTask;
734     }
735
736     myActiveTask = activeTask;
737     doActivate(myActiveTask, false);
738     myDispatcher.getMulticaster().taskActivated(myActiveTask);
739   }
740
741   private static LocalTaskImpl createDefaultTask() {
742     return new LocalTaskImpl(LocalTaskImpl.DEFAULT_TASK_ID, "Default task");
743   }
744
745   public void disposeComponent() {
746     if (myCacheRefreshTimer != null) {
747       myCacheRefreshTimer.stop();
748     }
749     myChangeListManager.removeChangeListListener(myChangeListListener);
750   }
751
752   public void updateIssues(final @Nullable Runnable onComplete) {
753     TaskRepository first = ContainerUtil.find(getAllRepositories(), new Condition<TaskRepository>() {
754       public boolean value(TaskRepository repository) {
755         return repository.isConfigured();
756       }
757     });
758     if (first == null) {
759       myIssueCache.clear();
760       if (onComplete != null) {
761         onComplete.run();
762       }
763       return;
764     }
765     myUpdating = true;
766     if (ApplicationManager.getApplication().isUnitTestMode()) {
767       doUpdate(onComplete);
768     }
769     else {
770       ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
771         public void run() {
772           doUpdate(onComplete);
773         }
774       });
775     }
776   }
777
778   private void doUpdate(@Nullable Runnable onComplete) {
779     try {
780       List<Task> issues = getIssuesFromRepositories(null, 0, myConfig.updateIssuesCount, false, false, new EmptyProgressIndicator());
781       if (issues == null) return;
782
783       synchronized (myIssueCache) {
784         myIssueCache.clear();
785         for (Task issue : issues) {
786           myIssueCache.put(issue.getId(), issue);
787         }
788       }
789       // update local tasks
790       synchronized (myTasks) {
791         for (Map.Entry<String, LocalTask> entry : myTasks.entrySet()) {
792           Task issue = myIssueCache.get(entry.getKey());
793           if (issue != null) {
794             entry.getValue().updateFromIssue(issue);
795           }
796         }
797       }
798     }
799     finally {
800       if (onComplete != null) {
801         onComplete.run();
802       }
803       myUpdating = false;
804     }
805   }
806
807   @Nullable
808   private List<Task> getIssuesFromRepositories(@Nullable String request,
809                                                int offset,
810                                                int limit,
811                                                boolean withClosed,
812                                                boolean forceRequest,
813                                                @NotNull final ProgressIndicator cancelled) {
814     List<Task> issues = null;
815     for (final TaskRepository repository : getAllRepositories()) {
816       if (!repository.isConfigured() || (!forceRequest && myBadRepositories.contains(repository))) {
817         continue;
818       }
819       try {
820         long start = System.currentTimeMillis();
821         Task[] tasks = repository.getIssues(request, offset, limit, withClosed, cancelled);
822         long timeSpent = System.currentTimeMillis() - start;
823         LOG.debug(String.format("Total %s ms to download %d issues from '%s' (pattern '%s')",
824                                 timeSpent, tasks.length, repository.getUrl(), request));
825         myBadRepositories.remove(repository);
826         if (issues == null) issues = new ArrayList<Task>(tasks.length);
827         if (!repository.isSupported(TaskRepository.NATIVE_SEARCH) && request != null) {
828           List<Task> filteredTasks = TaskSearchSupport.filterTasks(request, ContainerUtil.list(tasks));
829           ContainerUtil.addAll(issues, filteredTasks);
830         }
831         else {
832           ContainerUtil.addAll(issues, tasks);
833         }
834       }
835       catch (ProcessCanceledException ignored) {
836         // OK
837       }
838       catch (Exception e) {
839         String reason = "";
840         // Fix to IDEA-111810
841         //noinspection InstanceofCatchParameter
842         if (e.getClass() == Exception.class || e instanceof RequestFailedException) {
843           // probably contains some message meaningful to end-user
844           reason = e.getMessage();
845         }
846         //noinspection InstanceofCatchParameter
847         if (e instanceof SocketTimeoutException) {
848           LOG.warn("Socket timeout from " + repository);
849         }
850         else {
851           LOG.warn("Cannot connect to " + repository, e);
852         }
853         myBadRepositories.add(repository);
854         if (forceRequest) {
855           notifyAboutConnectionFailure(repository, reason);
856         }
857       }
858     }
859     return issues;
860   }
861
862   private void notifyAboutConnectionFailure(final TaskRepository repository, String details) {
863     Notifications.Bus.register(TASKS_NOTIFICATION_GROUP, NotificationDisplayType.BALLOON);
864     String content = "<p><a href=\"\">Configure server...</a></p>";
865     if (!StringUtil.isEmpty(details)) {
866       content = "<p>" + details + "</p>" + content;
867     }
868     Notifications.Bus.notify(new Notification(TASKS_NOTIFICATION_GROUP, "Cannot connect to " + repository.getUrl(),
869                                               content, NotificationType.WARNING,
870                                               new NotificationListener() {
871                                                 public void hyperlinkUpdate(@NotNull Notification notification,
872                                                                             @NotNull HyperlinkEvent event) {
873                                                   TaskRepositoriesConfigurable configurable =
874                                                     new TaskRepositoriesConfigurable(myProject);
875                                                   ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable);
876                                                   if (!ArrayUtil.contains(repository, getAllRepositories())) {
877                                                     notification.expire();
878                                                   }
879                                                 }
880                                               }), myProject);
881   }
882
883   @Override
884   public boolean isVcsEnabled() {
885     return ProjectLevelVcsManager.getInstance(myProject).getAllActiveVcss().length > 0;
886   }
887
888   @Override
889   public AbstractVcs getActiveVcs() {
890     AbstractVcs[] vcss = ProjectLevelVcsManager.getInstance(myProject).getAllActiveVcss();
891     if (vcss.length == 0) return null;
892     for (AbstractVcs vcs : vcss) {
893       if (vcs.getType() == VcsType.distributed) {
894         return vcs;
895       }
896     }
897     return vcss[0];
898   }
899
900   @Override
901   public boolean isLocallyClosed(@NotNull LocalTask localTask) {
902     if (isVcsEnabled()) {
903       List<ChangeListInfo> lists = localTask.getChangeLists();
904       if (lists.isEmpty()) return true;
905       for (ChangeListInfo list : lists) {
906         if (StringUtil.isEmpty(list.id)) {
907           return true;
908         }
909       }
910     }
911     return false;
912   }
913
914   @Nullable
915   @Override
916   public LocalTask getAssociatedTask(@NotNull LocalChangeList list) {
917     for (LocalTask task : getLocalTasks()) {
918       for (ChangeListInfo changeListInfo : task.getChangeLists()) {
919         if (changeListInfo.id.equals(list.getId())) {
920           return task;
921         }
922       }
923     }
924     return null;
925   }
926
927   @Override
928   public void trackContext(@NotNull LocalChangeList changeList) {
929     ChangeListInfo changeListInfo = new ChangeListInfo(changeList);
930     String changeListName = changeList.getName();
931     LocalTaskImpl task = createLocalTask(changeListName);
932     task.addChangelist(changeListInfo);
933     addTask(task);
934     if (changeList.isDefault()) {
935       activateTask(task, false);
936     }
937   }
938
939   @Override
940   public void disassociateFromTask(@NotNull LocalChangeList changeList) {
941     ChangeListInfo changeListInfo = new ChangeListInfo(changeList);
942     for (LocalTask localTask : getLocalTasks()) {
943       if (localTask.getChangeLists().contains(changeListInfo)) {
944         localTask.removeChangelist(changeListInfo);
945       }
946     }
947   }
948
949   public void decorateChangeList(@NotNull LocalChangeList changeList,
950                                  @NotNull ColoredTreeCellRenderer cellRenderer,
951                                  boolean selected,
952                                  boolean expanded,
953                                  boolean hasFocus) {
954     LocalTask task = getAssociatedTask(changeList);
955     if (task != null && task.isIssue()) {
956       cellRenderer.setIcon(task.getIcon());
957     }
958   }
959
960   public void createChangeList(@NotNull LocalTask task, String name) {
961     String comment = TaskUtil.getChangeListComment(task);
962     createChangeList(task, name, comment);
963   }
964
965   private void createChangeList(LocalTask task, String name, @Nullable String comment) {
966     LocalChangeList changeList = myChangeListManager.findChangeList(name);
967     if (changeList == null) {
968       changeList = myChangeListManager.addChangeList(name, comment);
969     }
970     else {
971       final LocalTask associatedTask = getAssociatedTask(changeList);
972       if (associatedTask != null) {
973         associatedTask.removeChangelist(new ChangeListInfo(changeList));
974       }
975       changeList.setComment(comment);
976     }
977     task.addChangelist(new ChangeListInfo(changeList));
978     myChangeListManager.setDefaultChangeList(changeList);
979   }
980
981   public String getChangelistName(Task task) {
982     String name = task.isIssue() && myConfig.changelistNameFormat != null
983                   ? TaskUtil.formatTask(task, myConfig.changelistNameFormat)
984                   : task.getSummary();
985     return StringUtil.shortenTextWithEllipsis(name, 100, 0);
986   }
987
988   public String suggestBranchName(Task task) {
989     if (task.isIssue()) {
990       return TaskUtil.formatTask(task, myConfig.branchNameFormat).replace(' ', '-');
991     }
992     else {
993       String summary = task.getSummary();
994       List<String> words = StringUtil.getWordsIn(summary);
995       String[] strings = ArrayUtil.toStringArray(words);
996       return StringUtil.join(strings, 0, Math.min(2, strings.length), "-");
997     }
998   }
999
1000
1001   @TestOnly
1002   public ChangeListAdapter getChangeListListener() {
1003     return myChangeListListener;
1004   }
1005
1006   /**
1007    * Reconfigure repository's HTTP clients probably to apply new connection settings.
1008    */
1009   public void reconfigureRepositoryClients() {
1010     for (TaskRepository repository : myRepositories) {
1011       if (repository instanceof BaseRepositoryImpl) {
1012         ((BaseRepositoryImpl)repository).reconfigureClient();
1013       }
1014     }
1015   }
1016
1017   public static class Config {
1018
1019     @Property(surroundWithTag = false)
1020     @AbstractCollection(surroundWithTag = false, elementTag = "task")
1021     public List<LocalTaskImpl> tasks = new ArrayList<LocalTaskImpl>();
1022
1023     public int localTasksCounter = 1;
1024
1025     public int taskHistoryLength = 50;
1026
1027     public boolean updateEnabled = true;
1028     public int updateInterval = 20;
1029     public int updateIssuesCount = 100;
1030
1031     // create task options
1032     public boolean clearContext = true;
1033     public boolean createChangelist = true;
1034     public boolean createBranch = true;
1035
1036     // close task options
1037     public boolean commitChanges = true;
1038     public boolean mergeBranch = true;
1039
1040     public boolean saveContextOnCommit = true;
1041     public boolean trackContextForNewChangelist = false;
1042
1043     public String changelistNameFormat = "{id} {summary}";
1044     public String branchNameFormat = "{id}";
1045
1046     public boolean searchClosedTasks = false;
1047     @Tag("servers")
1048     public Element servers = new Element("servers");
1049   }
1050
1051   private abstract class TestConnectionTask extends com.intellij.openapi.progress.Task.Modal {
1052
1053     protected Exception myException;
1054
1055     @Nullable
1056     protected TaskRepository.CancellableConnection myConnection;
1057
1058     public TestConnectionTask(String title) {
1059       super(TaskManagerImpl.this.myProject, title, true);
1060     }
1061
1062     @Override
1063     public void onCancel() {
1064       if (myConnection != null) {
1065         myConnection.cancel();
1066       }
1067     }
1068   }
1069 }