View Javadoc

1   /**
2    * Copyright (C) 2008 Atlassian
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.atlassian.theplugin.idea.crucible;
17  
18  import com.atlassian.theplugin.cfg.CfgUtil;
19  import com.atlassian.theplugin.commons.ServerType;
20  import com.atlassian.theplugin.commons.UiTaskExecutor;
21  import com.atlassian.theplugin.commons.cfg.CrucibleServerCfg;
22  import com.atlassian.theplugin.commons.cfg.ServerCfg;
23  import com.atlassian.theplugin.commons.crucible.CrucibleServerFacadeImpl;
24  import com.atlassian.theplugin.commons.crucible.api.model.*;
25  import com.atlassian.theplugin.commons.exception.ServerPasswordNotProvidedException;
26  import com.atlassian.theplugin.commons.remoteapi.RemoteApiException;
27  import com.atlassian.theplugin.commons.remoteapi.ServerData;
28  import com.atlassian.theplugin.configuration.CrucibleWorkspaceConfiguration;
29  import com.atlassian.theplugin.configuration.WorkspaceConfigurationBean;
30  import com.atlassian.theplugin.crucible.model.*;
31  import com.atlassian.theplugin.idea.Constants;
32  import com.atlassian.theplugin.idea.IdeaHelper;
33  import com.atlassian.theplugin.idea.PluginToolWindowPanel;
34  import com.atlassian.theplugin.idea.config.ProjectCfgManagerImpl;
35  import com.atlassian.theplugin.idea.crucible.editor.CommentHighlighter;
36  import com.atlassian.theplugin.idea.crucible.filters.CustomFilterChangeListener;
37  import com.atlassian.theplugin.idea.crucible.tree.*;
38  import com.atlassian.theplugin.idea.crucible.tree.node.CrucibleReviewTreeNode;
39  import com.atlassian.theplugin.idea.ui.PopupAwareMouseAdapter;
40  import com.atlassian.theplugin.idea.ui.tree.paneltree.TreeRenderer;
41  import com.atlassian.theplugin.idea.ui.tree.paneltree.TreeUISetup;
42  import com.atlassian.theplugin.util.PluginUtil;
43  import com.intellij.openapi.actionSystem.*;
44  import com.intellij.openapi.progress.ProgressIndicator;
45  import com.intellij.openapi.progress.ProgressManager;
46  import com.intellij.openapi.progress.Task;
47  import com.intellij.openapi.project.Project;
48  import com.intellij.ui.TreeSpeedSearch;
49  import org.jetbrains.annotations.NonNls;
50  import org.jetbrains.annotations.NotNull;
51  import org.jetbrains.annotations.Nullable;
52  
53  import javax.swing.*;
54  import javax.swing.event.DocumentEvent;
55  import javax.swing.event.DocumentListener;
56  import javax.swing.tree.DefaultMutableTreeNode;
57  import javax.swing.tree.TreeCellRenderer;
58  import javax.swing.tree.TreePath;
59  import java.awt.*;
60  import java.awt.event.*;
61  import java.net.MalformedURLException;
62  import java.net.URL;
63  import java.util.ArrayList;
64  import java.util.Collection;
65  import java.util.List;
66  
67  /**
68   * @author Jacek Jaroczynski
69   */
70  public class ReviewListToolWindowPanel extends PluginToolWindowPanel implements DataProvider {
71  
72  	public static final String PLACE_PREFIX = ReviewListToolWindowPanel.class.getSimpleName();
73  	private static final TreeCellRenderer TREE_RENDERER = new TreeRenderer();
74  
75  	private final CrucibleWorkspaceConfiguration crucibleProjectConfiguration;
76  	private ReviewTree reviewTree;
77  	private CrucibleFilterListModel filterListModel;
78  	private CrucibleFilterTreeModel filterTreeModel;
79  
80  	private CrucibleReviewGroupBy groupBy = CrucibleReviewGroupBy.NONE;
81  	private FilterTree filterTree;
82  	private CrucibleCustomFilterDetailsPanel detailsPanel;
83  	private SearchingCrucibleReviewListModel searchingReviewListModel;
84  	private final ProjectCfgManagerImpl projectCfgManager;
85  	private final UiTaskExecutor uiTaskExecutor;
86  	private final CrucibleReviewListModel reviewListModel;
87  	private Timer timer;
88  
89  	private static final int ONE_SECOND = 1000;
90  	private CrucibleReviewListModel currentReviewListModel;
91  
92  
93  	public ReviewListToolWindowPanel(@NotNull final Project project,
94  			@NotNull final WorkspaceConfigurationBean projectConfiguration,
95  			@NotNull final ProjectCfgManagerImpl projectCfgManager,
96  			@NotNull final CrucibleReviewListModel reviewListModel,
97  			@NotNull final UiTaskExecutor uiTaskExecutor) {
98  		super(project, "ThePlugin.Reviews.LeftToolBar", "ThePlugin.Reviews.RightToolBar");
99  		this.projectCfgManager = projectCfgManager;
100 		this.uiTaskExecutor = uiTaskExecutor;
101 
102 		crucibleProjectConfiguration = projectConfiguration.getCrucibleConfiguration();
103 
104 		filterListModel = new CrucibleFilterListModel(
105 				crucibleProjectConfiguration.getCrucibleFilters().getManualFilter(),
106 				crucibleProjectConfiguration.getCrucibleFilters().getRecenltyOpenFilter());
107 		filterTreeModel = new CrucibleFilterTreeModel(filterListModel, reviewListModel);
108 		this.reviewListModel = reviewListModel;
109 		CrucibleReviewListModel sortingListModel = new SortingByKeyCrucibleReviewListModel(this.reviewListModel);
110 		searchingReviewListModel = new SearchingCrucibleReviewListModel(sortingListModel);
111 		currentReviewListModel = searchingReviewListModel;
112 		init(Constants.DIALOG_MARGIN / 2);
113 		this.reviewListModel.addListener(new LocalCrucibleReviewListModelListener());
114 		filterTree.addSelectionListener(new LocalCrucibleFilterListModelListener());
115 	}
116 
117 	@Override
118 	public void init(int margin) {
119 		super.init(margin);
120 
121 		addReviewTreeListeners();
122 		setupReviewTree();
123 
124 		initToolBar();
125 
126 		detailsPanel = new CrucibleCustomFilterDetailsPanel(
127 				getProject(), projectCfgManager, crucibleProjectConfiguration,
128 				filterTree, CrucibleServerFacadeImpl.getInstance(), uiTaskExecutor);
129 		detailsPanel.addCustomFilterChangeListener(new CustomFilterChangeListener() {
130 			public void customFilterChanged(CustomFilter customFilter) {
131 				refresh(UpdateReason.FILTER_CHANGED);
132 			}
133 		});
134 
135 		filterTreeModel.nodeChanged((DefaultMutableTreeNode) filterTreeModel.getRoot());
136 
137 		if (crucibleProjectConfiguration != null
138 				&& crucibleProjectConfiguration.getCrucibleFilters().getManualFilter() != null
139 				&& crucibleProjectConfiguration.getCrucibleFilters().getManualFilter().isEnabled()) {
140 			showManualFilterPanel(true);
141 		} else {
142 			showManualFilterPanel(false);
143 		}
144 
145 		addSearchBoxListener();
146 	}
147 
148 	private void setupReviewTree() {
149 		TreeUISetup uiSetup = new TreeUISetup(TREE_RENDERER);
150 		uiSetup.initializeUI(reviewTree, getRightScrollPane());
151 	}
152 
153 	private void initToolBar() {
154 		// restore GroupBy setting
155 		if (crucibleProjectConfiguration.getView() != null && crucibleProjectConfiguration.getView().getGroupBy() != null) {
156 			groupBy = crucibleProjectConfiguration.getView().getGroupBy();
157 		}
158 		reviewTree.setGroupBy(groupBy);
159 	}
160 
161 	public void openReview(final ReviewAdapter review, boolean retrieveDetails) {
162 		CommentHighlighter.removeCommentsInEditors(project);
163 		reviewListModel.openReview(review, UpdateReason.OPEN_IN_IDE);
164 		IdeaHelper.getReviewDetailsToolWindow(getProject()).showReview(review, retrieveDetails);
165 	}
166 
167 	public void closeReviewDetailsWindow(final AnActionEvent event) {
168 		reviewListModel.clearOpenInIde(UpdateReason.OPEN_IN_IDE);
169 		IdeaHelper.getReviewDetailsToolWindow(project).closeToolWindow(event);
170 	}
171 
172 
173 	private void addReviewTreeListeners() {
174 		reviewTree.addKeyListener(new KeyAdapter() {
175 			@Override
176 			public void keyPressed(final KeyEvent e) {
177 				final ReviewAdapter review = reviewTree.getSelectedReview();
178 				if (e.getKeyCode() == KeyEvent.VK_ENTER && review != null) {
179 					openReview(review, true);
180 				}
181 			}
182 		});
183 
184 		reviewTree.addMouseListener(new PopupAwareMouseAdapter() {
185 
186 			@Override
187 			public void mouseClicked(final MouseEvent e) {
188 				final ReviewAdapter review = reviewTree.getSelectedReview();
189 				if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 && review != null) {
190 					openReview(review, true);
191 				}
192 			}
193 
194 			@Override
195 			protected void onPopup(MouseEvent e) {
196 				int selRow = reviewTree.getRowForLocation(e.getX(), e.getY());
197 				TreePath selPath = reviewTree.getPathForLocation(e.getX(), e.getY());
198 				if (selRow != -1 && selPath != null) {
199 					reviewTree.setSelectionPath(selPath);
200 					final ReviewAdapter review = reviewTree.getSelectedReview();
201 					if (review != null) {
202 						launchContextMenu(e);
203 					}
204 				}
205 			}
206 		});
207 	}
208 
209 	private void launchContextMenu(MouseEvent e) {
210 		final DefaultActionGroup actionGroup = new DefaultActionGroup();
211 
212 		final ActionGroup configActionGroup = (ActionGroup) ActionManager
213 				.getInstance().getAction("ThePlugin.Reviews.ReviewPopupMenu");
214 		actionGroup.addAll(configActionGroup);
215 
216 		final ActionPopupMenu popup = ActionManager.getInstance().createActionPopupMenu(getActionPlaceName(), actionGroup);
217 
218 		final JPopupMenu jPopupMenu = popup.getComponent();
219 		jPopupMenu.show(e.getComponent(), e.getX(), e.getY());
220 	}
221 
222 	private void setPanelEnabled(boolean enabled) {
223 		if (reviewTree != null) {
224 			reviewTree.setEnabled(enabled);
225 		}
226 		if (filterTree != null) {
227 			filterTree.setEnabled(enabled);
228 		}
229 		if (getSearchField() != null) {
230 			getSearchField().setEnabled(enabled);
231 		}
232 		if (getStatusBarPane() != null) {
233 			getStatusBarPane().setEnabled(enabled);
234 		}
235 		filterTree.redrawNodes();
236 	}
237 
238 	@Nullable
239 	public Object getData(@NonNls String dataId) {
240 		if (reviewTree != null) {
241 			if (dataId.equals(Constants.REVIEW)) {
242 				return reviewTree.getSelectedReview();
243 			} else if (dataId.equals(Constants.REVIEW_WINDOW_ENABLED)) {
244 				return reviewTree.isEnabled();
245 			}
246 			if (dataId.equals(Constants.SERVER)) {
247 				if (reviewTree.getSelectedReview() != null) {
248 					return reviewTree.getSelectedReview().getServerData();
249 				}
250 			}
251 		}
252 		return null;
253 	}
254 
255 	@Override
256 	protected void addSearchBoxListener() {
257 		getSearchField().addDocumentListener(new DocumentListener() {
258 			public void insertUpdate(DocumentEvent e) {
259 				triggerDelayedSearchBoxUpdate();
260 			}
261 
262 			public void removeUpdate(DocumentEvent e) {
263 				triggerDelayedSearchBoxUpdate();
264 			}
265 
266 			public void changedUpdate(DocumentEvent e) {
267 				triggerDelayedSearchBoxUpdate();
268 			}
269 		});
270 		getSearchField().addKeyboardListener(new KeyListener() {
271 			public void keyTyped(KeyEvent e) {
272 			}
273 
274 			public void keyPressed(KeyEvent e) {
275 				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
276 					getSearchField().addCurrentTextToHistory();
277 				}
278 			}
279 
280 			public void keyReleased(KeyEvent e) {
281 			}
282 		});
283 	}
284 
285 	private void triggerDelayedSearchBoxUpdate() {
286 		if (timer != null && timer.isRunning()) {
287 			return;
288 		}
289 		timer = new Timer(ONE_SECOND, new ActionListener() {
290 			public void actionPerformed(ActionEvent e) {
291 				searchingReviewListModel.setSearchTerm(getSearchField().getText());
292 			}
293 		});
294 		timer.setRepeats(false);
295 		timer.start();
296 	}
297 
298 	@Override
299 	public JTree createRightTree() {
300 		if (reviewTree == null) {
301 			reviewTree = new ReviewTree(new ReviewTreeModel(currentReviewListModel, projectCfgManager,
302 					CfgUtil.getProjectId(project)));
303 
304 			new TreeSpeedSearch(reviewTree) {
305 				@Override
306 				protected boolean isMatchingElement(Object o, String s) {
307 					TreePath tp = (TreePath) o;
308 					Object node = tp.getLastPathComponent();
309 					if (node instanceof CrucibleReviewTreeNode) {
310 						ReviewTreeNode rtn = (ReviewTreeNode) node;
311 						ReviewAdapter review = rtn.getReview();
312 						return review.getPermId().getId().toLowerCase().contains(s.toLowerCase())
313 								|| review.getName().toLowerCase().contains(s.toLowerCase());
314 					} else {
315 						return super.isMatchingElement(o, s);
316 					}
317 				}
318 			};
319 		}
320 		return reviewTree;
321 	}
322 
323 	@Override
324 	public JTree createLeftTree() {
325 		if (filterTree == null && filterTreeModel != null) {
326 			filterTree = new FilterTree(filterTreeModel, crucibleProjectConfiguration);
327 		}
328 
329 		return filterTree;
330 	}
331 
332 
333 	@Override
334 	public String getActionPlaceName() {
335 		return PLACE_PREFIX + this.getProject().getName();
336 	}
337 
338 	public void setGroupBy(CrucibleReviewGroupBy groupBy) {
339 		this.groupBy = groupBy;
340 		reviewTree.groupBy(groupBy);
341 
342 		crucibleProjectConfiguration.getView().setGroupBy(groupBy);
343 	}
344 
345 	public CrucibleReviewGroupBy getGroupBy() {
346 		return groupBy;
347 	}
348 
349 	protected void showManualFilterPanel(boolean visible) {
350 		getSplitLeftPane().setOrientation(true);
351 
352 		if (visible) {
353 			getSplitLeftPane().setSecondComponent(detailsPanel);
354 			getSplitLeftPane().setProportion(MANUAL_FILTER_PROPORTION_VISIBLE);
355 
356 		} else {
357 			getSplitLeftPane().setSecondComponent(null);
358 			getSplitLeftPane().setProportion(MANUAL_FILTER_PROPORTION_HIDDEN);
359 		}
360 	}
361 
362 	public void refresh(final UpdateReason reason) {
363 		Task.Backgroundable refresh = new Task.Backgroundable(getProject(), "Refreshing Crucible Panel", false) {
364 			@Override
365 			public void run(@NotNull final ProgressIndicator indicator) {
366 				reviewListModel.rebuildModel(reason);
367 			}
368 		};
369 		ProgressManager.getInstance().run(refresh);
370 	}
371 
372 	public Collection<CrucibleServerCfg> getServers() {
373 		return projectCfgManager.getCfgManager().getAllEnabledCrucibleServers(CfgUtil.getProjectId(project));
374 	}
375 
376 	public List<ReviewAdapter> getLocalReviews(final String searchKey) {
377 		List<ReviewAdapter> reviews = new ArrayList<ReviewAdapter>();
378 		for (ReviewAdapter review : currentReviewListModel.getReviews()) {
379 			if (searchKey.toUpperCase().equals(review.getPermId().getId().toUpperCase())) {
380 				reviews.add(review);
381 			}
382 		}
383 
384 		return reviews;
385 	}
386 
387 	public CrucibleWorkspaceConfiguration getCrucibleConfiguration() {
388 		return crucibleProjectConfiguration;
389 	}
390 
391 	/**
392 	 * Blocking method. Should be called in the background thread.
393 	 *
394 	 * @param recentlyOpenReviews list of recenlty open reviews
395 	 * @return list of review adapters
396 	 */
397 	public List<ReviewAdapter> getReviewAdapters(final List<ReviewRecentlyOpenBean> recentlyOpenReviews) {
398 
399 		List<ReviewAdapter> reviews = new ArrayList<ReviewAdapter>();
400 
401 		if (recentlyOpenReviews == null || recentlyOpenReviews.isEmpty()) {
402 			return reviews;
403 		}
404 
405 		for (ReviewRecentlyOpenBean recentlyOpenReview : recentlyOpenReviews) {
406 			// search local list for recently open reviews
407 			ReviewAdapter ra = getReviewFromLocalModel(recentlyOpenReview.getReviewId(), recentlyOpenReview.getServerId());
408 
409 			if (ra != null) {
410 				reviews.add(ra);
411 			} else {
412 				// search review on the servers
413 				ReviewAdapter rra = getReviewFromServer(recentlyOpenReview.getReviewId(), recentlyOpenReview.getServerId());
414 
415 				if (rra != null) {
416 					reviews.add(rra);
417 				}
418 			}
419 		}
420 
421 		return reviews;
422 	}
423 
424 	/**
425 	 * Blocking method. Should be called in the background thread.
426 	 *
427 	 * @param reviewKey review key
428 	 * @param serverId  server id
429 	 * @return review if found or null otherwise
430 	 */
431 	private ReviewAdapter getReviewFromServer(final String reviewKey, final String serverId) {
432 
433 		ServerCfg server = CfgUtil.getEnabledServerCfgbyServerId(project, projectCfgManager, serverId);
434 		if (server != null) {
435 			try {
436 				final ServerData serverData = projectCfgManager.getServerData(server);
437 				Review r = CrucibleServerFacadeImpl.getInstance().getReview(serverData, new PermIdBean(reviewKey));
438 				return new ReviewAdapter(r, serverData);
439 			} catch (RemoteApiException e) {
440 				PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
441 				setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
442 			} catch (ServerPasswordNotProvidedException e) {
443 				PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
444 				setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
445 			}
446 		}
447 
448 		return null;
449 	}
450 
451 	/**
452 	 * Blocking method. Should be called in the background thread.
453 	 *
454 	 * @param reviewKey review key
455 	 * @param serverId  server id
456 	 * @return review if found or null otherwise
457 	 */
458 	// todo remove that method if review contains details (ValueNotYetInitialized problem)
459 	private ReviewAdapter getReviewWithDetailsFromServer(final String reviewKey, final String serverId) {
460 
461 		ServerCfg server = CfgUtil.getEnabledServerCfgbyServerId(project, projectCfgManager, serverId);
462 		if (server != null) {
463 			try {
464 				final ServerData serverData = projectCfgManager.getServerData(server);
465 				Review r = CrucibleServerFacadeImpl.getInstance().getReview(serverData, new PermIdBean(reviewKey));
466 				ReviewAdapter ra = new ReviewAdapter(r, serverData);
467 				CrucibleServerFacadeImpl.getInstance().getDetailsForReview(ra);
468 				return ra;
469 			} catch (RemoteApiException e) {
470 				PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
471 				setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
472 			} catch (ServerPasswordNotProvidedException e) {
473 				PluginUtil.getLogger().warn("Exception thrown when retrieving review", e);
474 				setStatusErrorMessage("Cannot get review from the server: " + e.getMessage(), e);
475 			}
476 		}
477 
478 		return null;
479 	}
480 
481 	private ReviewAdapter getReviewFromLocalModel(final String reviewKey, final String serverId) {
482 		if (currentReviewListModel.getReviews() != null && !currentReviewListModel.getReviews().isEmpty()) {
483 			for (ReviewAdapter localReview : currentReviewListModel.getReviews()) {
484 				if (localReview.getPermId().getId().equals(reviewKey)
485 						&& localReview.getServerData().getServerId().equals(serverId)) {
486 					return localReview;
487 				}
488 			}
489 		}
490 		return null;
491 	}
492 
493 	/**
494 	 * Should be called from the background thread. It downloads review from the server if not found in the local model.
495 	 *
496 	 * @param reviewKey
497 	 * @param serverUrl
498 	 * @return review
499 	 */
500 	public ReviewAdapter openReviewWithDetails(final String reviewKey, final String serverUrl) {
501 		ServerData server = CfgUtil.findServer(serverUrl, projectCfgManager.getCfgManager().
502 				getAllServers(CfgUtil.getProjectId(project), ServerType.CRUCIBLE_SERVER), projectCfgManager);
503 
504 		if (server == null) {
505 			// server not found by exact url, trying to remove protocol from the address (http vs https) and slash at the end
506 			URL url;
507 
508 			try {
509 				url = new URL(serverUrl);
510 			} catch (MalformedURLException e) {
511 				PluginUtil.getLogger().warn("Error opening review. Invalid url [" + serverUrl + "]", e);
512 				return null;
513 			}
514 
515 			server = CfgUtil.findServer(url, projectCfgManager.getCfgManager().
516 					getAllServers(CfgUtil.getProjectId(project), ServerType.JIRA_SERVER), projectCfgManager);
517 		}
518 
519 		if (server != null) {
520 			// todo uncomment that if local review contains details
521 			// (will be implemented together with removed ValueNotYetInitialized)
522 //			ReviewAdapter ra = getReviewFromLocalModel(reviewKey, server.getServerId());
523 //			if (ra == null) {
524 //				ra = getReviewFromServer(reviewKey, server.getServerId());
525 //			}
526 
527 			ReviewAdapter ra = getReviewWithDetailsFromServer(reviewKey, server.getServerId());
528 
529 			if (ra != null) {
530 				final ReviewAdapter reviewAdapter = ra;
531 				EventQueue.invokeLater(new Runnable() {
532 					public void run() {
533 						openReview(reviewAdapter, false);
534 					}
535 				});
536 
537 				return ra;
538 			}
539 		}
540 		return null;
541 	}
542 
543 	private class LocalCrucibleFilterListModelListener extends CrucibleFilterSelectionListenerAdapter {
544 		public void filterSelectionChanged() {
545 			// restart checker
546 			refresh(UpdateReason.FILTER_CHANGED);
547 		}
548 
549 		public void selectedCustomFilter(CustomFilter customFilter) {
550 			showManualFilterPanel(true);
551 		}
552 
553 		public void unselectedCustomFilter() {
554 			showManualFilterPanel(false);
555 		}
556 	}
557 
558 	private class LocalCrucibleReviewListModelListener extends CrucibleReviewListModelListenerAdapter {
559 		private Exception exception;
560 
561 		@Override
562 		public void reviewListUpdateStarted(UpdateContext updateContext) {
563 			exception = null;
564 			if (updateContext.getUpdateReason() != UpdateReason.TIMER_FIRED) {
565 				setPanelEnabled(false);
566 				setStatusInfoMessage("Loading reviews...");
567 			}
568 		}
569 
570 		@Override
571 		public void reviewListUpdateFinished(UpdateContext updateContext) {
572 			setPanelEnabled(true);
573 			if (exception != null) {
574 				setStatusErrorMessage(exception.getMessage(), exception);
575 			} else {
576 				setStatusInfoMessage("Loaded " + reviewListModel.getReviews().size() + " reviews");
577 			}
578 		}
579 
580 		@Override
581 		public void reviewListUpdateError(final UpdateContext updateContext, final Exception e) {
582 			this.exception = e;
583 		}
584 	}
585 }