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