View Javadoc

1   /*
2    * This software was designed and created by Jason Carroll.
3    * Copyright (c) 2002, 2003, 2004 Jason Carroll.
4    * The author can be reached at jcarroll@cowsultants.com
5    * ITracker website: http://www.cowsultants.com
6    * ITracker forums: http://www.cowsultants.com/phpBB/index.php
7    *
8    * This program is free software; you can redistribute it and/or modify
9    * it only under the terms of the GNU General Public License as published by
10   * the Free Software Foundation; either version 2 of the License, or
11   * (at your option) any later version.
12   *
13   * This program is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of
15   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   * GNU General Public License for more details.
17   */
18  
19  package org.itracker.services.util;
20  
21  import java.text.SimpleDateFormat;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  import java.util.MissingResourceException;
30  import java.util.Set;
31  
32  import org.apache.log4j.Logger;
33  import org.itracker.core.resources.ITrackerResources;
34  import org.itracker.model.Configuration;
35  import org.itracker.model.CustomField;
36  import org.itracker.model.Issue;
37  import org.itracker.model.IssueActivityType;
38  import org.itracker.model.IssueRelation;
39  import org.itracker.model.NameValuePair;
40  import org.itracker.model.Notification;
41  import org.itracker.model.PermissionType;
42  import org.itracker.model.Project;
43  import org.itracker.model.User;
44  import org.itracker.web.util.ServletContextUtils;
45  
46  /**
47   * Contains utilities used when displaying and processing issues.
48   */
49  public class IssueUtilities {
50  
51  	private static final Logger log = Logger.getLogger(IssueUtilities.class);
52  	public static final int FIELD_TYPE_SINGLE = 1;
53  	public static final int FIELD_TYPE_INDEXED = 2;
54  	public static final int FIELD_TYPE_MAP = 3;
55  
56  	public static final int FIELD_ID = -1;
57  	public static final int FIELD_DESCRIPTION = -2;
58  	public static final int FIELD_STATUS = -3;
59  	public static final int FIELD_RESOLUTION = -4;
60  	public static final int FIELD_SEVERITY = -5;
61  	public static final int FIELD_CREATOR = -6;
62  	public static final int FIELD_CREATEDATE = -7;
63  	public static final int FIELD_OWNER = -8;
64  	public static final int FIELD_LASTMODIFIED = -9;
65  	public static final int FIELD_PROJECT = -10;
66  	public static final int FIELD_TARGET_VERSION = -11;
67  	public static final int FIELD_COMPONENTS = -12;
68  	public static final int FIELD_VERSIONS = -13;
69  	public static final int FIELD_ATTACHMENTDESCRIPTION = -14;
70  	public static final int FIELD_ATTACHMENTFILENAME = -15;
71  	public static final int FIELD_HISTORY = -16;
72  
73  	protected static final int[] STANDARD_FIELDS = { FIELD_ID,
74  			FIELD_DESCRIPTION, FIELD_STATUS, FIELD_RESOLUTION, FIELD_SEVERITY,
75  			FIELD_CREATOR, FIELD_CREATEDATE, FIELD_OWNER, FIELD_LASTMODIFIED,
76  			FIELD_PROJECT, FIELD_TARGET_VERSION, FIELD_COMPONENTS,
77  			FIELD_VERSIONS, FIELD_ATTACHMENTDESCRIPTION,
78  			FIELD_ATTACHMENTFILENAME, FIELD_HISTORY };
79  
80  	public static final int STATUS_NEW = 100;
81  	public static final int STATUS_UNASSIGNED = 200;
82  	public static final int STATUS_ASSIGNED = 300;
83  	public static final int STATUS_RESOLVED = 400;
84  	public static final int STATUS_CLOSED = 500;
85  
86  	// This marks the end of all status numbers. You can NOT add a status above
87  	// this number or
88  	// they will not be found.
89  	public static final int STATUS_END = 600;
90  
91  	public static final int HISTORY_STATUS_REMOVED = -1;
92  	public static final int HISTORY_STATUS_AVAILABLE = 1;
93  
94  	/** Defines a related issue. Sample text: related to */
95  	public static final int RELATION_TYPE_RELATED_P = 1;
96  	/** Defines a related issue. Sample text: related to */
97  	public static final int RELATION_TYPE_RELATED_C = 2;
98  	/** Defines a duplicate issue. Sample text: duplicates */
99  	public static final int RELATION_TYPE_DUPLICATE_P = 3;
100 	/** Defines a duplicate issue. Sample text: duplicate of */
101 	public static final int RELATION_TYPE_DUPLICATE_C = 4;
102 	/** Defines a cloned issue. Sample text: cloned to */
103 	public static final int RELATION_TYPE_CLONED_P = 5;
104 	/** Defines a cloned issue. Sample text: cloned from */
105 	public static final int RELATION_TYPE_CLONED_C = 6;
106 	/** Defines a split issue. Sample text: split to */
107 	public static final int RELATION_TYPE_SPLIT_P = 7;
108 	/** Defines a split issue. Sample text: split from */
109 	public static final int RELATION_TYPE_SPLIT_C = 8;
110 	/** Defines a dependent issue. Sample text: dependents */
111 	public static final int RELATION_TYPE_DEPENDENT_P = 9;
112 	/** Defines a dependent issue. Sample text: depends on */
113 	public static final int RELATION_TYPE_DEPENDENT_C = 10;
114 
115 	public static final int NUM_RELATION_TYPES = 10;
116 
117 	private static List<Configuration> resolutions = new ArrayList<Configuration>();
118 	private static List<Configuration> severities = new ArrayList<Configuration>();
119 	private static List<Configuration> statuses = new ArrayList<Configuration>();
120 	private static List<CustomField> customFields = new ArrayList<CustomField>();
121 	private static final Logger logger = Logger.getLogger(IssueUtilities.class);
122 
123 	public IssueUtilities() {
124 	}
125 
126 	public static int getFieldType(Integer fieldId) {
127 		if (fieldId != null) {
128 			if (fieldId.intValue() > 0) {
129 				return FIELD_TYPE_MAP;
130 			}
131 			/*
132 			 * switch(fieldId.intValue()) { case FIELD_COMPONENTS: return
133 			 * FIELD_TYPE_INDEXED; case FIELD_VERSIONS: return
134 			 * FIELD_TYPE_INDEXED; }
135 			 */
136 		}
137 
138 		return FIELD_TYPE_SINGLE;
139 	}
140 
141 	public static String getFieldName(Integer fieldId) {
142 		if (fieldId == null) {
143 			return "";
144 		}
145 
146 		if (fieldId.intValue() > 0) {
147 			return "customFields";
148 		}
149 
150 		switch (fieldId.intValue()) {
151 		case FIELD_ID:
152 			return "id";
153 		case FIELD_DESCRIPTION:
154 			return "description";
155 		case FIELD_STATUS:
156 			return "status";
157 		case FIELD_RESOLUTION:
158 			return "resolution";
159 		case FIELD_SEVERITY:
160 			return "severity";
161 		case FIELD_CREATOR:
162 			return "creatorId";
163 		case FIELD_CREATEDATE:
164 			return "createdate";
165 		case FIELD_OWNER:
166 			return "ownerId";
167 		case FIELD_LASTMODIFIED:
168 			return "lastmodified";
169 		case FIELD_PROJECT:
170 			return "projectId";
171 		case FIELD_TARGET_VERSION:
172 			return "targetVersion";
173 		case FIELD_COMPONENTS:
174 			return "components";
175 		case FIELD_VERSIONS:
176 			return "versions";
177 		case FIELD_ATTACHMENTDESCRIPTION:
178 			return "attachmentDescription";
179 		case FIELD_ATTACHMENTFILENAME:
180 			return "attachment";
181 		case FIELD_HISTORY:
182 			return "history";
183 		default:
184 			return "";
185 		}
186 	}
187 
188 	public static String getFieldName(Integer fieldId,
189 			List<CustomField> customFields, Locale locale) {
190 		if (fieldId.intValue() < 0) {
191 			return ITrackerResources.getString(getStandardFieldKey(fieldId
192 					.intValue()), locale);
193 		} else {
194 			for (int i = 0; i < customFields.size(); i++) {
195 				if (fieldId.equals(customFields.get(i).getId())) {
196 					return CustomFieldUtilities.getCustomFieldName(fieldId,
197 							locale);
198 				}
199 			}
200 		}
201 
202 		return ITrackerResources.getString("itracker.web.generic.unknown",
203 				locale);
204 	}
205 
206 	public static String getStandardFieldKey(int fieldId) {
207 		switch (fieldId) {
208 		case FIELD_ID:
209 			return "itracker.web.attr.id";
210 		case FIELD_DESCRIPTION:
211 			return "itracker.web.attr.description";
212 		case FIELD_STATUS:
213 			return "itracker.web.attr.status";
214 		case FIELD_RESOLUTION:
215 			return "itracker.web.attr.resolution";
216 		case FIELD_SEVERITY:
217 			return "itracker.web.attr.severity";
218 		case FIELD_CREATOR:
219 			return "itracker.web.attr.creator";
220 		case FIELD_CREATEDATE:
221 			return "itracker.web.attr.createdate";
222 		case FIELD_OWNER:
223 			return "itracker.web.attr.owner";
224 		case FIELD_LASTMODIFIED:
225 			return "itracker.web.attr.lastmodified";
226 		case FIELD_PROJECT:
227 			return "itracker.web.attr.project";
228 		case FIELD_TARGET_VERSION:
229 			return "itracker.web.attr.target";
230 		case FIELD_COMPONENTS:
231 			return "itracker.web.attr.components";
232 		case FIELD_VERSIONS:
233 			return "itracker.web.attr.versions";
234 		case FIELD_ATTACHMENTDESCRIPTION:
235 			return "itracker.web.attr.attachmentdescription";
236 		case FIELD_ATTACHMENTFILENAME:
237 			return "itracker.web.attr.attachmentfilename";
238 		case FIELD_HISTORY:
239 			return "itracker.web.attr.detaileddescription";
240 		default:
241 			return "itracker.web.generic.unknown";
242 		}
243 	}
244 
245 	public static NameValuePair[] getStandardFields(Locale locale) {
246 		NameValuePair[] fieldNames = new NameValuePair[STANDARD_FIELDS.length];
247 		for (int i = 0; i < STANDARD_FIELDS.length; i++) {
248 			fieldNames[i] = new NameValuePair(ITrackerResources.getString(
249 					getStandardFieldKey(STANDARD_FIELDS[i]), locale), Integer
250 					.toString(STANDARD_FIELDS[i]));
251 		}
252 		return fieldNames;
253 	}
254 
255 	public static String getRelationName(int value) {
256 		return getRelationName(value, ITrackerResources.getLocale());
257 	}
258 
259 	public static String getRelationName(int value, Locale locale) {
260 		return getRelationName(Integer.toString(value), locale);
261 	}
262 
263 	public static String getRelationName(String value, Locale locale) {
264 		return ITrackerResources.getString(
265 				ITrackerResources.KEY_BASE_ISSUE_RELATION + value, locale);
266 	}
267 
268 	public static int getMatchingRelationType(int relationType) {
269 		switch (relationType) {
270 		case RELATION_TYPE_RELATED_P:
271 			return RELATION_TYPE_RELATED_C;
272 		case RELATION_TYPE_RELATED_C:
273 			return RELATION_TYPE_RELATED_P;
274 		case RELATION_TYPE_DUPLICATE_P:
275 			return RELATION_TYPE_DUPLICATE_C;
276 		case RELATION_TYPE_DUPLICATE_C:
277 			return RELATION_TYPE_DUPLICATE_P;
278 		case RELATION_TYPE_CLONED_P:
279 			return RELATION_TYPE_CLONED_C;
280 		case RELATION_TYPE_CLONED_C:
281 			return RELATION_TYPE_CLONED_P;
282 		case RELATION_TYPE_SPLIT_P:
283 			return RELATION_TYPE_SPLIT_C;
284 		case RELATION_TYPE_SPLIT_C:
285 			return RELATION_TYPE_SPLIT_P;
286 		case RELATION_TYPE_DEPENDENT_P:
287 			return RELATION_TYPE_DEPENDENT_C;
288 		case RELATION_TYPE_DEPENDENT_C:
289 			return RELATION_TYPE_DEPENDENT_P;
290 		default:
291 			return -1;
292 		}
293 	}
294 
295 	public static String componentsToString(Issue issue) {
296 		StringBuffer value = new StringBuffer();
297 		if (issue != null && issue.getComponents().size() > 0) {
298 			for (int i = 0; i < issue.getComponents().size(); i++) {
299 				value.append((i != 0 ? ", " : "")
300 						+ issue.getComponents().get(i).getName());
301 			}
302 		}
303 		return value.toString();
304 	}
305 
306 	public static String versionsToString(Issue issue) {
307 		StringBuffer value = new StringBuffer();
308 		if (issue != null && issue.getVersions().size() > 0) {
309 			for (int i = 0; i < issue.getVersions().size(); i++) {
310 				value.append((i != 0 ? ", " : "")
311 						+ issue.getVersions().get(i).getNumber());
312 			}
313 		}
314 		return value.toString();
315 	}
316 
317 	public static String historyToString(Issue issue, SimpleDateFormat sdf) {
318 		StringBuffer value = new StringBuffer();
319 		if (issue != null && issue.getHistory().size() > 0 && sdf != null) {
320 			for (int i = 0; i < issue.getHistory().size(); i++) {
321 				value.append((i != 0 ? "," : "")
322 						+ issue.getHistory().get(i).getDescription() + ","
323 						+ issue.getHistory().get(i).getUser().getFirstName());
324 				value.append(" "
325 						+ issue.getHistory().get(i).getUser().getLastName()
326 						+ ","
327 						+ sdf.format(issue.getHistory().get(i)
328 								.getLastModifiedDate()));
329 			}
330 		}
331 		return value.toString();
332 	}
333 
334 	public static String getStatusName(Integer value) {
335 		return getStatusName(value, ITrackerResources.getLocale());
336 	}
337 
338 	public static String getStatusName(int value, Locale locale) {
339 		return getStatusName(Integer.toString(value), locale);
340 	}
341 
342 	public static String getStatusName(String value, Locale locale) {
343 		return ITrackerResources.getString(ITrackerResources.KEY_BASE_STATUS
344 				+ value, locale);
345 	}
346 
347 	/**
348 	 * getStatuses() needs to get implemented..
349 	 * 
350 	 */
351 	public static List<Configuration> getStatuses() {
352 		return statuses;
353 	}
354 
355 	public static List<NameValuePair> getStatuses(Locale locale) {
356 		NameValuePair[] statusStrings = new NameValuePair[statuses.size()];
357 		for (int i = 0; i < statuses.size(); i++) {
358 			statusStrings[i] = new NameValuePair(ITrackerResources.getString(
359 					ITrackerResources.KEY_BASE_STATUS
360 							+ statuses.get(i).getValue(), locale), statuses
361 					.get(i).getValue());
362 		}
363 		return Arrays.asList(statusStrings);
364 	}
365 
366 	public static void setStatuses(List<Configuration> value) {
367 		statuses = (value == null ? new ArrayList<Configuration>() : value);
368 	}
369 
370 	public static int getNumberStatuses() {
371 		return statuses.size();
372 	}
373 
374 	public static String getSeverityName(Integer value) {
375 		return getSeverityName(value, ITrackerResources.getLocale());
376 	}
377 
378 	public static String getSeverityName(int value, Locale locale) {
379 		return getSeverityName(Integer.toString(value), locale);
380 	}
381 
382 	public static String getSeverityName(String value, Locale locale) {
383 		return ITrackerResources.getString(ITrackerResources.KEY_BASE_SEVERITY
384 				+ value, locale);
385 	}
386 
387 	/**
388 	 * Returns the list of the defined issue severities in the system. The array
389 	 * returned is a cached list set from the setSeverities method. The actual
390 	 * values are stored in the database and and can be obtained from the
391 	 * ConfigurationService bean.
392 	 * 
393 	 * @param locale
394 	 *            the locale to return the severities as
395 	 * @returns array of translated strings from the cached severities list
396 	 */
397 	public static List<NameValuePair> getSeverities(Locale locale) {
398 		List<NameValuePair> severityStrings = new ArrayList<NameValuePair>();
399 
400 		for (int i = 0; i < severities.size(); i++) {
401 			String string1 = ITrackerResources.getString(
402 					ITrackerResources.KEY_BASE_SEVERITY
403 							+ severities.get(i).getValue(), locale);
404 			String string2 = severities.get(i).getValue();
405 			NameValuePair nvp = new NameValuePair(string1, string2);
406 			severityStrings.add(i, nvp);
407 		}
408 		return severityStrings;
409 	}
410 
411 	public static void setSeverities(List<Configuration> value) {
412 		severities = (value == null ? new ArrayList<Configuration>() : value);
413 	}
414 
415 	public static int getNumberSeverities() {
416 		return severities.size();
417 	}
418 
419 	/**
420 	 * Compares the severity of two issues. The int returned will be negative if
421 	 * the the severity of issue A is less than the severity of issue B,
422 	 * positive if issue A is a higher severity than issue B, or 0 if the two
423 	 * issues have the same severity.
424 	 * 
425 	 * @param issueA
426 	 *            IssueModel A
427 	 * @param issueB
428 	 *            IssueModel B
429 	 * @returns an int representing the compared severities
430 	 */
431 	public static int compareSeverity(Issue issueA, Issue issueB) {
432 		if (issueA == null && issueB == null) {
433 			return 0;
434 		} else if (issueA == null && issueB != null) {
435 			return -1;
436 		} else if (issueA != null && issueB == null) {
437 			return 1;
438 		} else {
439 			int issueAIndex = Integer.MAX_VALUE;
440 			int issueBIndex = Integer.MAX_VALUE;
441 			for (int i = 0; i < severities.size(); i++) {
442 				if (severities.get(i) != null) {
443 					if (severities.get(i).getValue().equalsIgnoreCase(
444 							Integer.toString(issueA.getSeverity()))) {
445 						issueAIndex = i;
446 					}
447 					if (severities.get(i).getValue().equalsIgnoreCase(
448 							Integer.toString(issueB.getSeverity()))) {
449 						issueBIndex = i;
450 					}
451 				}
452 			}
453 			if (issueAIndex > issueBIndex) {
454 				return -1;
455 			} else if (issueAIndex < issueBIndex) {
456 				return 1;
457 			}
458 		}
459 
460 		return 0;
461 	}
462 
463 	public static String getResolutionName(int value) {
464 		return getResolutionName(value, ITrackerResources.getLocale());
465 	}
466 
467 	public static String getResolutionName(int value, Locale locale) {
468 		return getResolutionName(Integer.toString(value), locale);
469 	}
470 
471 	public static String getResolutionName(String value, Locale locale) {
472 		return ITrackerResources.getString(
473 				ITrackerResources.KEY_BASE_RESOLUTION + value, locale);
474 	}
475 
476 	public static String checkResolutionName(String value, Locale locale)
477 			throws MissingResourceException {
478 		return ITrackerResources.getCheckForKey(
479 				ITrackerResources.KEY_BASE_RESOLUTION + value, locale);
480 	}
481 
482 	/**
483 	 * Returns the list of predefined resolutions in the system. The array
484 	 * returned is a cached list set from the setResolutions method. The actual
485 	 * values are stored in the database and and can be obtained from the
486 	 * ConfigurationService bean.
487 	 * 
488 	 * @param locale
489 	 *            the locale to return the resolutions as
490 	 * @returns array of translated strings from the cached resolution list
491 	 */
492 	public static List<NameValuePair> getResolutions(Locale locale) {
493 		NameValuePair[] resolutionStrings = new NameValuePair[resolutions
494 				.size()];
495 		for (int i = 0; i < resolutions.size(); i++) {
496 			resolutionStrings[i] = new NameValuePair(ITrackerResources
497 					.getString(ITrackerResources.KEY_BASE_RESOLUTION
498 							+ resolutions.get(i).getValue(), locale),
499 					resolutions.get(i).getValue());
500 		}
501 		return Arrays.asList(resolutionStrings);
502 	}
503 
504 	/**
505 	 * Sets the cached list of predefined resolutions.
506 	 */
507 	public static void setResolutions(List<Configuration> value) {
508 		resolutions = (value == null ? new ArrayList<Configuration>() : value);
509 	}
510 
511 	public static String getActivityName(IssueActivityType type) {
512 		return getActivityName(type, ITrackerResources.getLocale());
513 	}
514 
515 	public static String getActivityName(IssueActivityType type, Locale locale) {
516 		return ITrackerResources.getString("itracker.activity."
517 				+ String.valueOf(type.name()), locale);
518 	}
519 
520 	/**
521 	 * Returns the cached array of CustomFieldModels.
522 	 * 
523 	 * @return an array of CustomFieldModels
524 	 */
525 	public static List<CustomField> getCustomFields() {
526 		return (customFields == null ? new ArrayList<CustomField>()
527 				: customFields);
528 	}
529 
530 	/**
531 	 * Sets the cached array of CustomFieldModels.
532 	 * 
533 	 * @return an array of CustomFieldModels
534 	 */
535 	public static void setCustomFields(List<CustomField> value) {
536 		customFields = (value == null ? new ArrayList<CustomField>() : value);
537 	}
538 
539 //	/**
540 //	 * Returns an array of the cached custom fields. The fields labels will be
541 //	 * initialized based on the Locale given. If no locale is given, the default
542 //	 * locale of the system will be used.
543 //	 * 
544 //	 * @param locale
545 //	 *            the locale to use to populate the field labels
546 //	 * @return the cached array of CustomFieldModels
547 //	 */
548 //	public static List<CustomField> getCustomFields(Locale locale) {
549 //		CustomField[] localizedFields = new CustomField[customFields.size()];
550 //		for (int i = 0; i < customFields.size(); i++) {
551 //			try {
552 //				localizedFields[i] = (CustomField) customFields.get(i).clone();
553 //				if (localizedFields[i] != null) {
554 //					localizedFields[i].setLabels(locale);
555 //				}
556 //			} catch (CloneNotSupportedException cnse) {
557 //				logger.error("Error cloning CustomField: " + cnse.getMessage());
558 //			}
559 //		}
560 //		return Arrays.asList(localizedFields);
561 //	}
562 
563 	/**
564 	 * Returns the custom field with the supplied id. Any labels will be
565 	 * localized to the system default locale.
566 	 * 
567 	 * @param bitValue
568 	 *            the id of the field to return
569 	 * @return the requested CustomField object, or a new field if not found
570 	 */
571 	public static CustomField getCustomField(Integer id) {
572 		return getCustomField(id, ITrackerResources.getLocale());
573 	}
574 
575 	/**
576 	 * Returns the custom field with the supplied id value. Any labels will be
577 	 * translated to the given locale.
578 	 * 
579 	 * @param id
580 	 *            the id of the field to return
581 	 * @param locale
582 	 *            the locale to initialize any labels with
583 	 * @return the requested CustomField object, or a new field if not found
584 	 */
585 	public static CustomField getCustomField(Integer id, Locale locale) {
586 		CustomField retField = null;
587 
588 		try {
589 			for (int i = 0; i < customFields.size(); i++) {
590 				if (customFields.get(i) != null
591 						&& customFields.get(i).getId() != null
592 						&& customFields.get(i).getId().equals(id)) {
593 					retField = (CustomField) customFields.get(i).clone();
594 					break;
595 				}
596 			}
597 		} catch (CloneNotSupportedException cnse) {
598 			logger.error("Error cloning CustomField: " + cnse.getMessage());
599 		}
600 		if (retField == null) {
601 			retField = new CustomField();
602 		}
603 
604 		return retField;
605 	}
606 
607 	/**
608 	 * Returns the total number of defined custom fields
609 	 */
610 	public static int getNumberCustomFields() {
611 		return customFields.size();
612 	}
613 
614 	/**
615 	 * Returns true if the user has permission to view the requested issue.
616 	 * 
617 	 * @param issue
618 	 *            an IssueModel of the issue to check view permission for
619 	 * @param user
620 	 *            a User for the user to check permission for
621 	 * @param permissions
622 	 *            a HashMap of the users permissions
623 	 */
624 	public static boolean canViewIssue(Issue issue, User user,
625 			Map<Integer, Set<PermissionType>> permissions) {
626 		if (user == null) {
627 			if (log.isInfoEnabled()) {
628 				log
629 						.info("canViewIssue: missing argument. user is null returning false");
630 			}
631 			return false;
632 		}
633 		return canViewIssue(issue, user.getId(), permissions);
634 	}
635 
636 	/**
637 	 * Returns true if the user has permission to view the requested issue.
638 	 * 
639 	 * @param issue
640 	 *            an IssueModel of the issue to check view permission for
641 	 * @param userId
642 	 *            the userId of the user to check permission for
643 	 * @param permissions
644 	 *            a HashMap of the users permissions
645 	 */
646 	public static boolean canViewIssue(Issue issue, Integer userId,
647 			Map<Integer, Set<PermissionType>> permissions) {
648 		if (issue == null || userId == null || permissions == null) {
649 			if (log.isInfoEnabled()) {
650 				log.info("canViewIssue: missing argument. issue: " + issue
651 						+ ", userid: " + userId + ", permissions: "
652 						+ permissions);
653 			}
654 			return false;
655 		}
656 
657 		if (UserUtilities.hasPermission(permissions,
658 				issue.getProject().getId(), PermissionType.ISSUE_VIEW_ALL
659 						.getCode())) {
660 			if (log.isInfoEnabled()) {
661 				log.info("canViewIssue: issue: " + issue + ", user: " + userId
662 						+ ", permission: " + PermissionType.ISSUE_VIEW_ALL);
663 			}
664 			return true;
665 		}
666 
667 		// I think owner & creator should always be able to view the issue
668 		// otherwise it makes no sense of creating the issue itself.
669 		// So put these checks before checking permissions for the whole project.  
670 		if (issue.getCreator().getId().equals(userId)) {
671 			if (log.isInfoEnabled()) {
672 				log.info("canViewIssue: issue: " + issue + ", user: " + userId
673 						+ ", permission: is creator");
674 			}
675 			return true;
676 		}
677 
678 		if (issue.getOwner() != null) {
679 			if (issue.getOwner().getId().equals(userId)) {
680 
681 				if (log.isInfoEnabled()) {
682 					log.info("canViewIssue: issue: " + issue + ", user: "
683 							+ userId + ", permission: is owner");
684 				}
685 				return true;
686 			}
687 		}
688 		
689 		if (!UserUtilities.hasPermission(permissions, issue.getProject()
690 				.getId(), PermissionType.ISSUE_VIEW_USERS.getCode())) {
691 			if (log.isInfoEnabled()) {
692 				log.info("canViewIssue: issue: " + issue + ", user: " + userId
693 						+ ", permission: " + PermissionType.ISSUE_VIEW_USERS);
694 			}
695 			
696 			return false;
697 		}
698 		
699 		if (log.isInfoEnabled()) {
700 			log.info("canViewIssue: issue: " + issue + ", user: " + userId
701 					+ ", permission: none matched");
702 		}
703 		return false;
704 	}
705 
706 	/**
707 	 * Returns true if the user has permission to edit the requested issue.
708 	 * 
709 	 * @param issue
710 	 *            an IssueModel of the issue to check edit permission for
711 	 * @param userId
712 	 *            the userId of the user to check permission for
713 	 * @param permissions
714 	 *            a HashMap of the users permissions
715 	 */
716 	public static boolean canEditIssue(Issue issue, Integer userId,
717 			Map<Integer, Set<PermissionType>> permissions) {
718 		if (issue == null || userId == null || permissions == null) {
719 			if (log.isInfoEnabled()) {
720 				log.info("canEditIssue: missing argument. issue: " + issue
721 						+ ", userid: " + userId + ", permissions: "
722 						+ permissions);
723 			}
724 			return false;
725 		}
726 
727 		// if (log.isDebugEnabled()) {
728 		//        	
729 		// StringBuffer sb = new StringBuffer();
730 		// Iterator<Integer> it = permissions.keySet().iterator();
731 		// Integer key;
732 		// Set<PermissionType> value;
733 		// while (it.hasNext()) {
734 		// key = it.next();
735 		// value = permissions.get(key);
736 		// sb.append(key).append(": ").append(value).append('\n');
737 		// }
738 		//        	
739 		// log.debug("canEditIssue: detailed permissions: \n" + sb);
740 		// }
741 
742 		if (UserUtilities.hasPermission(permissions,
743 				issue.getProject().getId(), PermissionType.ISSUE_EDIT_ALL
744 						.getCode())) {
745 
746 			if (log.isDebugEnabled()) {
747 				log.debug("canEditIssue: user " + userId
748 						+ " has permission to edit issue " + issue.getId()
749 						+ ":" + PermissionType.ISSUE_EDIT_ALL);
750 			}
751 			return true;
752 		}
753 		if (!UserUtilities.hasPermission(permissions, issue.getProject()
754 				.getId(), PermissionType.ISSUE_EDIT_USERS.getCode())) {
755 			if (log.isDebugEnabled()) {
756 				log.debug("canEditIssue: user " + userId
757 						+ " has not permission  to edit issue " + issue.getId()
758 						+ ":" + PermissionType.ISSUE_EDIT_USERS);
759 			}
760 			return false;
761 		}
762 
763 		if (issue.getCreator().getId().equals(userId)) {
764 			if (log.isDebugEnabled()) {
765 				log.debug("canEditIssue: user " + userId
766 						+ " is creator of issue " + issue.getId() + ":");
767 			}
768 			return true;
769 		}
770 		if (issue.getOwner() != null) {
771 			if (issue.getOwner().getId().equals(userId)) {
772 				if (log.isDebugEnabled()) {
773 					log.debug("canEditIssue: user " + userId
774 							+ " is owner of issue " + issue.getId() + ":");
775 				}
776 				return true;
777 			}
778 		}
779 
780 		if (log.isDebugEnabled()) {
781 			log.debug("canEditIssue: user " + userId
782 					+ " could not match permission, denied");
783 		}
784 		return false;
785 	}
786 
787 	/**
788 	 * Returns true if the user can be assigned to this issue.
789 	 * 
790 	 * @param issue
791 	 *            an IssueModel of the issue to check assign permission for
792 	 * @param userId
793 	 *            the userId of the user to check permission for
794 	 * @param permissions
795 	 *            a HashMap of the users permissions
796 	 */
797 	public static boolean canBeAssignedIssue(Issue issue, Integer userId,
798 			Map<Integer, Set<PermissionType>> permissions) {
799 		if (issue == null || userId == null || permissions == null) {
800 			return false;
801 		}
802 
803 		if (UserUtilities.hasPermission(permissions,
804 				issue.getProject().getId(), PermissionType.ISSUE_EDIT_ALL
805 						.getCode())) {
806 			return true;
807 		}
808 		if (UserUtilities.hasPermission(permissions,
809 				issue.getProject().getId(), PermissionType.ISSUE_EDIT_USERS
810 						.getCode())) {
811 			if (issue.getCreator().getId().equals(userId)) {
812 				return true;
813 			} else if (UserUtilities.hasPermission(permissions, issue
814 					.getProject().getId(), PermissionType.ISSUE_ASSIGNABLE
815 					.getCode())) {
816 				return true;
817 			} else if (issue.getOwner().getId() != null
818 					&& issue.getOwner().getId().equals(userId)) {
819 				return true;
820 			}
821 		}
822 
823 		return false;
824 	}
825 
826 	/**
827 	 * Returns true if the user can unassign themselves from the issue.
828 	 * 
829 	 * @param issue
830 	 *            an IssueModel of the issue to check assign permission for
831 	 * @param userId
832 	 *            the userId of the user to check permission for
833 	 * @param permissions
834 	 *            a HashMap of the users permissions
835 	 */
836 	public static boolean canUnassignIssue(Issue issue, Integer userId,
837 			Map<Integer, Set<PermissionType>> permissions) {
838 		if (issue == null || userId == null || permissions == null) {
839 			return false;
840 		}
841 
842 		if (UserUtilities.hasPermission(permissions,
843 				issue.getProject().getId(), PermissionType.ISSUE_ASSIGN_OTHERS
844 						.getCode())) {
845 			return true;
846 		}
847 		if (issue.getOwner() != null
848 				&& userId.equals(issue.getOwner().getId())
849 				&& UserUtilities.hasPermission(permissions, issue.getProject()
850 						.getId(), PermissionType.ISSUE_UNASSIGN_SELF.getCode())) {
851 			return true;
852 		}
853 
854 		return false;
855 	}
856 
857 	public static boolean hasIssueRelation(Issue issue, Integer relatedIssueId) {
858 		if (issue != null) {
859 			List<IssueRelation> relations = issue.getRelations();
860 			for (int i = 0; i < relations.size(); i++) {
861 				if (relations.get(i).getRelatedIssue().getId().equals(
862 						relatedIssueId)) {
863 					return true;
864 				}
865 			}
866 		}
867 		return false;
868 	}
869 
870 	public static boolean hasIssueNotification(Issue issue, Integer userId) {
871 		return hasIssueNotification(issue, issue.getProject(), userId);
872 	}
873 
874 	/**
875 	 * Evaluate if a certain user is notified on issue change.
876 	 * 
877 	 * FIXME: Does not work for admin of unassigned-issue-projects owner, see portalhome.do 
878 	 * @param issue
879 	 * @param project
880 	 * @param userId
881 	 * @return
882 	 */
883 	public static boolean hasIssueNotification(Issue issue, Project project,
884 			Integer userId) {
885 		if (issue == null || userId == null) {
886 			return false;
887 		}
888 
889 		
890 		if ((issue.getOwner() != null && issue.getOwner().getId().equals(userId))
891 				|| issue.getCreator().getId().equals(userId)) {
892 			return true;
893 		}
894 
895 		if (project != null && project.getOwners() != null && !project.getOwners().isEmpty()) {
896 			Iterator<User> owners = project.getOwners().iterator();
897 			while (owners.hasNext()) {
898 				if (owners.next().getId().equals(userId)) {
899 					return true;
900 				}
901 			}
902 		}
903 
904 		Collection<Notification> notifications = ServletContextUtils.getItrackerServices().getNotificationService().getIssueNotifications(issue);
905 		if (notifications != null && !notifications.isEmpty()) {
906 			Iterator<Notification> notificationsIt = notifications.iterator();
907 			while (notificationsIt.hasNext()) {
908 				if (notificationsIt.next().getUser().getId().equals(userId)) {
909 					return true;
910 				}
911 			}	
912 		}
913 		
914 		return false;
915 	}
916 }
917 
918