1 package org.argeo.tracker.core;
2
3 import static javax.jcr.Property.JCR_DESCRIPTION;
4 import static javax.jcr.Property.JCR_TITLE;
5 import static javax.jcr.PropertyType.DATE;
6 import static javax.jcr.PropertyType.STRING;
7 import static org.argeo.connect.ConnectNames.CONNECT_UID;
8 import static org.argeo.connect.util.ConnectJcrUtils.get;
9 import static org.argeo.tracker.TrackerNames.TRACKER_PARENT_UID;
10 import static org.argeo.tracker.TrackerNames.TRACKER_PROJECT_UID;
11
12 import java.text.DateFormat;
13 import java.text.SimpleDateFormat;
14 import java.util.ArrayList;
15 import java.util.Calendar;
16 import java.util.GregorianCalendar;
17 import java.util.List;
18
19 import javax.jcr.Node;
20 import javax.jcr.NodeIterator;
21 import javax.jcr.Property;
22 import javax.jcr.PropertyType;
23 import javax.jcr.RepositoryException;
24 import javax.jcr.Session;
25 import javax.jcr.nodetype.NodeType;
26 import javax.jcr.query.Query;
27 import javax.jcr.security.Privilege;
28
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.argeo.activities.ActivitiesException;
32 import org.argeo.activities.ActivitiesNames;
33 import org.argeo.activities.ActivitiesService;
34 import org.argeo.cms.CmsTypes;
35 import org.argeo.cms.auth.CurrentUser;
36 import org.argeo.cms.util.UserAdminUtils;
37 import org.argeo.connect.ConnectNames;
38 import org.argeo.connect.core.AbstractAppService;
39 import org.argeo.connect.util.ConnectJcrUtils;
40 import org.argeo.connect.util.ConnectUtils;
41 import org.argeo.connect.util.RemoteJcrUtils;
42 import org.argeo.connect.util.XPathUtils;
43 import org.argeo.eclipse.ui.EclipseUiUtils;
44 import org.argeo.jcr.JcrUtils;
45 import org.argeo.tracker.TrackerConstants;
46 import org.argeo.tracker.TrackerException;
47 import org.argeo.tracker.TrackerNames;
48 import org.argeo.tracker.TrackerService;
49 import org.argeo.tracker.TrackerTypes;
50
51 public class TrackerServiceImpl extends AbstractAppService implements TrackerService {
52 private final static Log log = LogFactory.getLog(TrackerServiceImpl.class);
53
54 private ActivitiesService activitiesService;
55
56 @Override
57 public synchronized Node publishEntity(Node parent, String nodeType, Node srcNode, boolean removeSrcNode)
58 throws RepositoryException {
59 Node createdNode = null;
60 if (TrackerTypes.TRACKER_ISSUE.equals(nodeType) || TrackerTypes.TRACKER_TASK.equals(nodeType)
61 || TrackerTypes.TRACKER_COMPONENT.equals(nodeType) || TrackerTypes.TRACKER_MILESTONE.equals(nodeType)
62 || TrackerTypes.TRACKER_VERSION.equals(nodeType)) {
63 Session session = parent.getSession();
64 Node project = getEntityByUid(session, null, get(srcNode, TRACKER_PROJECT_UID));
65 if (TrackerTypes.TRACKER_ISSUE.equals(nodeType) || TrackerTypes.TRACKER_TASK.equals(nodeType))
66 createIssueIdIfNeeded(project, srcNode);
67 String relPath = getDefaultRelPath(srcNode);
68 createdNode = JcrUtils.mkdirs(project, relPath);
69 RemoteJcrUtils.copy(srcNode, createdNode, true);
70 createdNode.addMixin(nodeType);
71 JcrUtils.updateLastModified(createdNode);
72 if (removeSrcNode)
73 srcNode.remove();
74 } else if (TrackerTypes.TRACKER_PROJECT.equals(nodeType) || TrackerTypes.TRACKER_IT_PROJECT.equals(nodeType)) {
75 String relPath = getDefaultRelPath(srcNode);
76 createdNode = JcrUtils.mkdirs(parent, relPath);
77 RemoteJcrUtils.copy(srcNode, createdNode, true);
78 createdNode.addMixin(nodeType);
79 JcrUtils.updateLastModified(createdNode);
80 if (removeSrcNode)
81 srcNode.remove();
82 }
83 return createdNode;
84 }
85
86 @Override
87 public String getAppBaseName() {
88 return TrackerConstants.TRACKER_APP_BASE_NAME;
89 }
90
91 @Override
92 public String getBaseRelPath(String nodeType) {
93 if (TrackerTypes.TRACKER_PROJECT.equals(nodeType) || TrackerTypes.TRACKER_IT_PROJECT.equals(nodeType)
94 || TrackerTypes.TRACKER_MILESTONE.equals(nodeType) || TrackerTypes.TRACKER_VERSION.equals(nodeType)
95 || TrackerTypes.TRACKER_COMPONENT.equals(nodeType) || TrackerTypes.TRACKER_ISSUE.equals(nodeType)
96 || TrackerTypes.TRACKER_TASK.equals(nodeType))
97 return TrackerNames.TRACKER_PROJECTS;
98 else
99 return getAppBaseName();
100 }
101
102 @Override
103 public String getDefaultRelPath(Node entity) throws RepositoryException {
104 if (entity.isNodeType(TrackerTypes.TRACKER_TASK)) {
105 String issueIdStr = ConnectJcrUtils.get(entity, TrackerNames.TRACKER_ID);
106 return TrackerNames.TRACKER_ISSUES + "/" + issueIdStr;
107 } else if (entity.isNodeType(TrackerTypes.TRACKER_PROJECT)
108 || entity.isNodeType(TrackerTypes.TRACKER_IT_PROJECT)) {
109 String title = entity.getProperty(Property.JCR_TITLE).getString();
110 String name = cleanTitle(title);
111 return name;
112 } else if (entity.isNodeType(TrackerTypes.TRACKER_MILESTONE)
113 || entity.isNodeType(TrackerTypes.TRACKER_VERSION)) {
114 String title = entity.getProperty(Property.JCR_TITLE).getString();
115 String name = cleanTitle(title);
116 return TrackerNames.TRACKER_MILESTONES + "/" + name;
117 } else if (entity.isNodeType(TrackerTypes.TRACKER_COMPONENT)) {
118 String title = entity.getProperty(Property.JCR_TITLE).getString();
119 String name = cleanTitle(title);
120 return TrackerNames.TRACKER_COMPONENTS + "/" + name;
121 }
122 return null;
123 }
124
125 @Override
126 public String getDefaultRelPath(Session session, String nodeType, String id) {
127
128 return null;
129 }
130
131 private static final String[] KNOWN_MIXIN = { TrackerTypes.TRACKER_PROJECT, TrackerTypes.TRACKER_IT_PROJECT,
132 TrackerTypes.TRACKER_ISSUE, TrackerTypes.TRACKER_TASK, TrackerTypes.TRACKER_COMMENT,
133 TrackerTypes.TRACKER_VERSION, TrackerTypes.TRACKER_MILESTONE, TrackerTypes.TRACKER_COMPONENT };
134
135 @Override
136 public String getMainNodeType(Node entity) {
137
138 for (String mixin : KNOWN_MIXIN)
139 if (ConnectJcrUtils.isNodeType(entity, mixin))
140 return mixin;
141 return null;
142 }
143
144 @Override
145 public boolean isKnownType(String nodeType) {
146 for (String mixin : KNOWN_MIXIN)
147 if (mixin.equals(nodeType))
148 return true;
149 return false;
150 }
151
152 @Override
153 public boolean isKnownType(Node entity) {
154 for (String mixin : KNOWN_MIXIN)
155 if (ConnectJcrUtils.isNodeType(entity, mixin))
156 return true;
157 return false;
158 }
159
160 @Override
161 public void configureCustomACL(Node node) {
162 try {
163 if (node.isNodeType(TrackerTypes.TRACKER_PROJECT) || node.isNodeType(TrackerTypes.TRACKER_IT_PROJECT)) {
164 Session session = node.getSession();
165
166 String basePath = node.getPath();
167 String counterpartyGroupId = node.getProperty(TrackerNames.TRACKER_CP_GROUP_ID).getString();
168 JcrUtils.addPrivilege(session, basePath, counterpartyGroupId, Privilege.JCR_READ);
169 JcrUtils.addPrivilege(session, basePath + "/" + TrackerNames.TRACKER_ISSUES, counterpartyGroupId,
170 Privilege.JCR_ALL);
171 session.save();
172 }
173 } catch (RepositoryException re) {
174 throw new TrackerException("Cannot onfigure ACL on" + node, re);
175 }
176 }
177
178
179 @Override
180 public void configureItProject(Node itProject, String title, String description, String managerId,
181 String counterpartyGroupId) throws RepositoryException {
182 itProject.setProperty(TrackerNames.TRACKER_CP_GROUP_ID, counterpartyGroupId);
183 itProject.setProperty(Property.JCR_TITLE, title);
184 itProject.setProperty(Property.JCR_DESCRIPTION, description);
185 JcrUtils.mkdirs(itProject, TrackerNames.TRACKER_DATA, NodeType.NT_FOLDER);
186 JcrUtils.mkdirs(itProject, TrackerNames.TRACKER_SPEC, CmsTypes.CMS_TEXT);
187 JcrUtils.mkdirs(itProject, TrackerNames.TRACKER_MILESTONES);
188 JcrUtils.mkdirs(itProject, TrackerNames.TRACKER_ISSUES);
189
190 if (itProject.getSession().hasPendingChanges())
191 JcrUtils.updateLastModified(itProject);
192 }
193
194 public void configureProject(Node project, String title, String description, String managerId)
195 throws RepositoryException {
196 ConnectJcrUtils.setJcrProperty(project, JCR_TITLE, STRING, title);
197 ConnectJcrUtils.setJcrProperty(project, JCR_DESCRIPTION, STRING, description);
198
199 ConnectJcrUtils.setJcrProperty(project, TrackerNames.TRACKER_MANAGER, STRING, managerId);
200
201 if (project.getSession().hasPendingChanges())
202 JcrUtils.updateLastModified(project);
203 }
204
205 @Override
206 public void configureTask(Node task, Node project, Node milestone, String title, String description,
207 String managerId) throws RepositoryException {
208 activitiesService.configureTask(task, TrackerTypes.TRACKER_TASK, title, description, managerId);
209 task.setProperty(TrackerNames.TRACKER_PROJECT_UID, project.getProperty(ConnectNames.CONNECT_UID).getString());
210 if (milestone != null)
211 task.setProperty(TrackerNames.TRACKER_MILESTONE_UID,
212 milestone.getProperty(ConnectNames.CONNECT_UID).getString());
213 else if (task.hasProperty(TrackerNames.TRACKER_MILESTONE_UID))
214 task.getProperty(TrackerNames.TRACKER_MILESTONE_UID).remove();
215
216 activitiesService.setTaskDefaultStatus(task, TrackerTypes.TRACKER_TASK);
217
218 if (task.getSession().hasPendingChanges())
219 JcrUtils.updateLastModified(task);
220 }
221
222 @Override
223 public void configureIssue(Node issue, Node project, Node milestone, String title, String description,
224 List<String> versionIds, List<String> componentIds, int priority, int importance, String managerId)
225 throws RepositoryException {
226 activitiesService.configureTask(issue, TrackerTypes.TRACKER_ISSUE, title, description, managerId);
227
228 issue.setProperty(TrackerNames.TRACKER_PROJECT_UID, project.getProperty(ConnectNames.CONNECT_UID).getString());
229 issue.setProperty(TrackerNames.TRACKER_PRIORITY, priority);
230 issue.setProperty(TrackerNames.TRACKER_IMPORTANCE, importance);
231 if (milestone != null) {
232 issue.setProperty(TrackerNames.TRACKER_MILESTONE_UID,
233 milestone.getProperty(ConnectNames.CONNECT_UID).getString());
234
235
236
237
238 } else {
239 if (issue.hasProperty(TrackerNames.TRACKER_MILESTONE_UID))
240 issue.getProperty(TrackerNames.TRACKER_MILESTONE_UID).remove();
241
242
243 }
244
245 if (versionIds != null && !versionIds.isEmpty()) {
246 issue.setProperty(TrackerNames.TRACKER_VERSION_IDS, versionIds.toArray(new String[0]));
247 }
248 if (componentIds != null && !componentIds.isEmpty())
249 issue.setProperty(TrackerNames.TRACKER_COMPONENT_IDS, componentIds.toArray(new String[0]));
250
251 if (issue.getSession().hasPendingChanges())
252 JcrUtils.updateLastModified(issue);
253 }
254
255 @Override
256 public void configureMilestone(Node milestone, Node project, Node parentMilestone, String title, String description,
257 String managerId, String defaultAssigneeId, Calendar targetDate) throws RepositoryException {
258 ConnectJcrUtils.setJcrProperty(milestone, TRACKER_PROJECT_UID, STRING, get(project, CONNECT_UID));
259 if (parentMilestone != null)
260 ConnectJcrUtils.setJcrProperty(milestone, TRACKER_PARENT_UID, STRING, get(parentMilestone, CONNECT_UID));
261 else if (milestone.hasProperty(TRACKER_PARENT_UID))
262 milestone.getProperty(TRACKER_PARENT_UID).remove();
263 ConnectJcrUtils.setJcrProperty(milestone, JCR_TITLE, STRING, title);
264 ConnectJcrUtils.setJcrProperty(milestone, JCR_DESCRIPTION, STRING, description);
265
266 ConnectJcrUtils.setJcrProperty(milestone, TrackerNames.TRACKER_MANAGER, STRING, managerId);
267 ConnectJcrUtils.setJcrProperty(milestone, TrackerNames.TRACKER_DEFAULT_ASSIGNEE, STRING, defaultAssigneeId);
268 ConnectJcrUtils.setJcrProperty(milestone, TrackerNames.TRACKER_TARGET_DATE, DATE, targetDate);
269
270 if (milestone.getSession().hasPendingChanges())
271 JcrUtils.updateLastModified(milestone);
272 }
273
274 @Override
275 public void configureVersion(Node version, Node project, String id, String description, Calendar releaseDate)
276 throws RepositoryException {
277 ConnectJcrUtils.setJcrProperty(version, TRACKER_PROJECT_UID, STRING, get(project, CONNECT_UID));
278 ConnectJcrUtils.setJcrProperty(version, TrackerNames.TRACKER_ID, STRING, id);
279 if (!version.isNodeType(TrackerTypes.TRACKER_MILESTONE))
280 ConnectJcrUtils.setJcrProperty(version, JCR_TITLE, STRING, id);
281 ConnectJcrUtils.setJcrProperty(version, JCR_DESCRIPTION, STRING, description);
282 ConnectJcrUtils.setJcrProperty(version, TrackerNames.TRACKER_RELEASE_DATE, DATE, releaseDate);
283
284 if (version.getSession().hasPendingChanges())
285 JcrUtils.updateLastModified(version);
286 }
287
288 @Override
289 public NodeIterator getMyProjects(Session session, boolean onlyOpenProjects) {
290 List<String> normalisedRoles = new ArrayList<>();
291 for (String role : CurrentUser.roles())
292 normalisedRoles.add(TrackerUtils.normalizeDn(role));
293 String[] nrArr = normalisedRoles.toArray(new String[0]);
294 return getProjectsForGroup(session, nrArr, onlyOpenProjects);
295 }
296
297 private NodeIterator getProjectsForGroup(Session session, String[] roles, boolean onlyOpenProjects) {
298 try {
299
300 StringBuilder builder = new StringBuilder();
301 builder.append("//element(*, ").append(TrackerTypes.TRACKER_PROJECT).append(")");
302
303
304 StringBuilder tmpBuilder = new StringBuilder();
305 for (String role : roles) {
306 String attrQuery = XPathUtils.getPropertyEquals(TrackerNames.TRACKER_MANAGER, role);
307 if (ConnectUtils.notEmpty(attrQuery))
308 tmpBuilder.append(attrQuery).append(" or ");
309 }
310 String groupCond = null;
311 if (tmpBuilder.length() > 4)
312 groupCond = "(" + tmpBuilder.substring(0, tmpBuilder.length() - 3) + ")";
313
314
315 String notClosedCond = null;
316 if (onlyOpenProjects)
317 notClosedCond = "not(@" + ConnectNames.CONNECT_CLOSE_DATE + ")";
318
319 String allCond = XPathUtils.localAnd(groupCond, notClosedCond);
320 if (EclipseUiUtils.notEmpty(allCond))
321 builder.append("[").append(allCond).append("]");
322
323 builder.append(" order by @").append(Property.JCR_LAST_MODIFIED).append(" descending");
324 if (log.isTraceEnabled())
325 log.trace("Getting open project list for " + CurrentUser.getDisplayName() + " (DN: "
326 + CurrentUser.getUsername() + ") with query: " + builder.toString());
327 Query query = XPathUtils.createQuery(session, builder.toString());
328 return query.execute().getNodes();
329 } catch (RepositoryException e) {
330 throw new ActivitiesException("Unable to get milestones for groups " + roles.toString());
331 }
332 }
333
334 public NodeIterator getMyMilestones(Session session, boolean onlyOpenMilestones) {
335 List<String> normalisedRoles = new ArrayList<>();
336 for (String role : CurrentUser.roles())
337 normalisedRoles.add(TrackerUtils.normalizeDn(role));
338 String[] nrArr = normalisedRoles.toArray(new String[0]);
339 return getMilestonesForGroup(session, nrArr, onlyOpenMilestones);
340 }
341
342 private NodeIterator getMilestonesForGroup(Session session, String[] roles, boolean onlyOpenMilestones) {
343 try {
344
345 StringBuilder builder = new StringBuilder();
346 builder.append("//element(*, ").append(TrackerTypes.TRACKER_MILESTONE).append(")");
347
348
349 StringBuilder tmpBuilder = new StringBuilder();
350 for (String role : roles) {
351 String attrQuery = XPathUtils.getPropertyEquals(TrackerNames.TRACKER_MANAGER, role);
352
353 tmpBuilder.append(attrQuery).append(" or ");
354 }
355 String groupCond = null;
356 if (tmpBuilder.length() > 4)
357 groupCond = "(" + tmpBuilder.substring(0, tmpBuilder.length() - 3) + ")";
358
359
360 String notClosedCond = null;
361 if (onlyOpenMilestones)
362 notClosedCond = "not(@" + ConnectNames.CONNECT_CLOSE_DATE + ")";
363
364 String allCond = XPathUtils.localAnd(groupCond, notClosedCond);
365 if (EclipseUiUtils.notEmpty(allCond))
366 builder.append("[").append(allCond).append("]");
367
368 builder.append(" order by @").append(Property.JCR_LAST_MODIFIED).append(" descending");
369 if (log.isTraceEnabled())
370 log.trace("Getting open milestone list for " + CurrentUser.getDisplayName() + " (DN: "
371 + CurrentUser.getUsername() + ") with query: " + builder.toString());
372 Query query = XPathUtils.createQuery(session, builder.toString());
373 return query.execute().getNodes();
374 } catch (RepositoryException e) {
375 throw new ActivitiesException("Unable to get milestones for groups " + roles.toString());
376 }
377 }
378
379 private final static DateFormat isobdf = new SimpleDateFormat(TrackerConstants.ISO_BASIC_DATE_FORMAT);
380
381 @Override
382 public Node addComment(Node parentIssue, String description) throws RepositoryException {
383 Node comments = JcrUtils.mkdirs(parentIssue, TrackerNames.TRACKER_COMMENTS);
384 String reporterId = parentIssue.getSession().getUserID();
385 String currUid = UserAdminUtils.getUserLocalId(reporterId);
386 Calendar creationDate = new GregorianCalendar();
387 String timeStamp = isobdf.format(creationDate.getTime());
388 Node comment = comments.addNode(timeStamp + "_" + currUid);
389 comment.addMixin(TrackerTypes.TRACKER_COMMENT);
390 ConnectJcrUtils.setJcrProperty(comment, Property.JCR_DESCRIPTION, PropertyType.STRING, description);
391
392
393 comment.setProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE, creationDate);
394 comment.setProperty(ActivitiesNames.ACTIVITIES_REPORTED_BY, reporterId);
395 return comment;
396 }
397
398 @Override
399 public boolean updateComment(Node comment, String newDescription) throws RepositoryException {
400 boolean hasChanged = ConnectJcrUtils.setJcrProperty(comment, Property.JCR_DESCRIPTION, PropertyType.STRING,
401 newDescription);
402 if (hasChanged)
403 JcrUtils.updateLastModified(comment);
404 return hasChanged;
405 }
406
407 @Override
408 public Node createVersion(Node project, String versionId, String description, Calendar targetDate,
409 Calendar releaseDate) throws RepositoryException {
410 Node version = JcrUtils.mkdirs(project, TrackerUtils.versionsRelPath() + "/" + versionId,
411 NodeType.NT_UNSTRUCTURED);
412 version.addMixin(TrackerTypes.TRACKER_VERSION);
413 version.setProperty(TrackerNames.TRACKER_ID, versionId);
414 version.setProperty(Property.JCR_TITLE, versionId);
415 if (EclipseUiUtils.notEmpty(description))
416 version.setProperty(Property.JCR_DESCRIPTION, description);
417 if (targetDate != null)
418 version.setProperty(TrackerNames.TRACKER_TARGET_DATE, targetDate);
419 if (releaseDate != null)
420 version.setProperty(TrackerNames.TRACKER_RELEASE_DATE, releaseDate);
421 return version;
422 }
423
424 @Override
425 public Node createComponent(Node project, String officeId, String title, String description)
426 throws RepositoryException {
427 Node component = JcrUtils.mkdirs(project, TrackerUtils.componentsRelPath() + "/" + officeId,
428 NodeType.NT_UNSTRUCTURED);
429 component.addMixin(TrackerTypes.TRACKER_COMPONENT);
430 component.setProperty(TrackerNames.TRACKER_ID, officeId);
431 component.setProperty(Property.JCR_TITLE, title);
432 if (EclipseUiUtils.notEmpty(description))
433 component.setProperty(Property.JCR_DESCRIPTION, description);
434 return component;
435 }
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460 protected long createIssueIdIfNeeded(Node project, Node issue) throws RepositoryException {
461 Long issueId = ConnectJcrUtils.getLongValue(issue, TrackerNames.TRACKER_ID);
462 if (issueId == null) {
463 String xpathQueryStr = XPathUtils.descendantFrom(project.getPath()) + "//element(*, "
464 + TrackerTypes.TRACKER_TASK + ")";
465 xpathQueryStr += " order by @" + TrackerNames.TRACKER_ID + " descending";
466 Query query = XPathUtils.createQuery(project.getSession(), xpathQueryStr);
467 query.setLimit(1);
468 NodeIterator nit = query.execute().getNodes();
469 issueId = 1l;
470 if (nit.hasNext())
471 issueId = ConnectJcrUtils.getLongValue(nit.nextNode(), TrackerNames.TRACKER_ID) + 1;
472 issue.setProperty(TrackerNames.TRACKER_ID, issueId);
473 }
474 return issueId;
475 }
476
477
478 private static String cleanTitle(String title) {
479 String name = title.replaceAll("[^a-zA-Z0-9]", "");
480 return name;
481 }
482
483
484 public void setActivitiesService(ActivitiesService activitiesService) {
485 this.activitiesService = activitiesService;
486 }
487
488 }