View Javadoc
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 		// TODO Auto-generated method stub
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 				// TODO refine privileges for client group
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 	/** No check is done to see if a similar project already exists */
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 		// TODO check if users are really existing
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 		// TODO Useless?
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 			// String targetId = ConnectJcrUtils.get(milestone,
235 			// TrackerNames.TRACKER_ID);
236 			// if (EclipseUiUtils.notEmpty(targetId))
237 			// issue.setProperty(TrackerNames.TRACKER_MILESTONE_ID, targetId);
238 		} else {
239 			if (issue.hasProperty(TrackerNames.TRACKER_MILESTONE_UID))
240 				issue.getProperty(TrackerNames.TRACKER_MILESTONE_UID).remove();
241 			// if (issue.hasProperty(TrackerNames.TRACKER_MILESTONE_ID))
242 			// issue.getProperty(TrackerNames.TRACKER_MILESTONE_ID).remove();
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 		// TODO check if users are really existing
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 			// XPath
300 			StringBuilder builder = new StringBuilder();
301 			builder.append("//element(*, ").append(TrackerTypes.TRACKER_PROJECT).append(")");
302 
303 			// Assigned to
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 			// Only open
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 			// XPath
345 			StringBuilder builder = new StringBuilder();
346 			builder.append("//element(*, ").append(TrackerTypes.TRACKER_MILESTONE).append(")");
347 
348 			// Assigned to
349 			StringBuilder tmpBuilder = new StringBuilder();
350 			for (String role : roles) {
351 				String attrQuery = XPathUtils.getPropertyEquals(TrackerNames.TRACKER_MANAGER, role);
352 //				if (StringUtils.notEmpty(attrQuery))
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 			// Only open
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 		// We rather use these activity like properties to be robuster in case
392 		// of migration for instance
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 	// private static Node getProjectFromIssue(Node issue) throws
438 	// RepositoryException {
439 	// Node parent = issue;
440 	// while (!parent.isNodeType(TrackerTypes.TRACKER_PROJECT)) {
441 	// parent = parent.getParent();
442 	// }
443 	// return parent;
444 	// }
445 
446 	// private static Node getIssueParent(Node project) throws
447 	// RepositoryException {
448 	// // Should always be there
449 	// return project.getNode(TrackerUtils.issuesRelPath());
450 	// }
451 
452 	// private static Node getVersionParent(Node project) throws
453 	// RepositoryException {
454 	// // Should always be there
455 	// return project.getNode(TrackerUtils.versionsRelPath());
456 	// }
457 
458 	// FIXME harden to avoid discrepancy in numbering while having concurrent
459 	// access
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 	// Local helpers
478 	private static String cleanTitle(String title) {
479 		String name = title.replaceAll("[^a-zA-Z0-9]", "");
480 		return name;
481 	}
482 
483 	/* DEPENDENCY INJECTION */
484 	public void setActivitiesService(ActivitiesService activitiesService) {
485 		this.activitiesService = activitiesService;
486 	}
487 
488 }