View Javadoc
1   package org.argeo.activities.core;
2   
3   import java.util.ArrayList;
4   import java.util.Calendar;
5   import java.util.GregorianCalendar;
6   import java.util.List;
7   
8   import javax.jcr.Node;
9   import javax.jcr.NodeIterator;
10  import javax.jcr.Property;
11  import javax.jcr.RepositoryException;
12  import javax.jcr.Session;
13  import javax.jcr.query.Query;
14  
15  import org.apache.commons.logging.Log;
16  import org.apache.commons.logging.LogFactory;
17  import org.argeo.activities.ActivitiesException;
18  import org.argeo.activities.ActivitiesNames;
19  import org.argeo.activities.ActivitiesService;
20  import org.argeo.activities.ActivitiesTypes;
21  import org.argeo.activities.ActivityValueCatalogs;
22  import org.argeo.cms.auth.CurrentUser;
23  import org.argeo.cms.util.UserAdminUtils;
24  import org.argeo.connect.ConnectNames;
25  import org.argeo.connect.UserAdminService;
26  import org.argeo.connect.core.AbstractAppService;
27  import org.argeo.connect.resources.ResourcesService;
28  import org.argeo.connect.util.ConnectJcrUtils;
29  import org.argeo.connect.util.ConnectUtils;
30  import org.argeo.connect.util.XPathUtils;
31  import org.argeo.eclipse.ui.EclipseUiUtils;
32  import org.argeo.jcr.JcrUtils;
33  
34  /** Concrete access to Connect's {@link ActivitiesService} */
35  public class ActivitiesServiceImpl extends AbstractAppService implements ActivitiesService, ActivitiesNames {
36  	private final static Log log = LogFactory.getLog(ActivitiesServiceImpl.class);
37  
38  	/* DEPENDENCY INJECTION */
39  	private UserAdminService userAdminService;
40  	private ResourcesService resourcesService;
41  
42  	/* API METHODS */
43  	@Override
44  	public String getAppBaseName() {
45  		return ActivitiesNames.ACTIVITIES_APP_BASE_NAME;
46  	}
47  
48  	@Override
49  	public String getDefaultRelPath(Node entity) throws RepositoryException {
50  		if (entity.isNodeType(ActivitiesTypes.ACTIVITIES_ACTIVITY)) {
51  			String currentUser = null;
52  			if (entity.hasProperty(ACTIVITIES_REPORTED_BY))
53  				currentUser = entity.getProperty(ACTIVITIES_REPORTED_BY).getString();
54  			else {
55  				currentUser = CurrentUser.getUsername();
56  				if (log.isDebugEnabled())
57  					log.warn("Activity at " + entity.getPath() + "has no reportedBy property");
58  			}
59  			String userId = UserAdminUtils.getUserLocalId(currentUser);
60  
61  			Calendar currentTime = null;
62  			if (entity.hasProperty(ACTIVITIES_ACTIVITY_DATE))
63  				currentTime = entity.getProperty(ACTIVITIES_ACTIVITY_DATE).getDate();
64  			else {
65  				currentTime = entity.getProperty(Property.JCR_CREATED).getDate();
66  				if (log.isDebugEnabled())
67  					log.warn("Activity at " + entity.getPath() + "has no activity date ");
68  			}
69  
70  			String nodeType = getMainNodeType(entity);
71  
72  			return dateAsRelPath(currentTime) + "/" + userId + "/" + nodeType;
73  		}
74  		return null;
75  	}
76  
77  	@Override
78  	public String getDefaultRelPath(Session session, String nodeType, String id) {
79  		throw new ActivitiesException("This method should not be used anymore");
80  		// if (ActivitiesTypes.ACTIVITIES_ACTIVITY.equals(nodeType)) {
81  		// String userId = session.getUserID();
82  		// Calendar currentTime = GregorianCalendar.getInstance();
83  		// return dateAsRelPath(currentTime) + "/" + userId;
84  		// } else
85  		// return null;
86  	}
87  
88  	// @Override
89  	// public Node publishEntity(Node parent, String nodeType, Node srcNode,
90  	// boolean removeSrcNode)
91  	// throws RepositoryException {
92  	// // TODO Auto-generated method stub
93  	// return null;
94  	// }
95  
96  	@Override
97  	public Node configureActivity(Node activity, String type, String title, String desc, List<Node> relatedTo)
98  			throws RepositoryException {
99  		return configureActivity(activity, activity.getSession().getUserID(), type, title, desc, relatedTo,
100 				new GregorianCalendar());
101 	}
102 
103 	@Override
104 	public Node configureActivity(Node activity, String reporterId, String type, String title, String desc,
105 			List<Node> relatedTo, Calendar date) throws RepositoryException {
106 		// try {
107 		// Node activityBase = session.getNode(getBasePath());
108 		// String localId = UserAdminUtils.getUserLocalId(reporterId);
109 		// Node activity = JcrUtils.mkdirs(activityBase,
110 		// getDefaultRelPath(session, ActivitiesTypes.ACTIVITIES_ACTIVITY,
111 		// localId));
112 		// activity.addMixin(type);
113 
114 		activity.setProperty(ActivitiesNames.ACTIVITIES_REPORTED_BY, reporterId);
115 
116 		// Activity Date
117 		if (date != null)
118 			activity.setProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE, date);
119 
120 		// related to
121 		if (relatedTo != null && !relatedTo.isEmpty())
122 			ConnectJcrUtils.setMultipleReferences(activity, ActivitiesNames.ACTIVITIES_RELATED_TO, relatedTo);
123 
124 		// Content
125 		activity.setProperty(Property.JCR_TITLE, title);
126 		activity.setProperty(Property.JCR_DESCRIPTION, desc);
127 		JcrUtils.updateLastModified(activity);
128 		return activity;
129 		// } catch (RepositoryException e) {
130 		// throw new ActivitiesException("Unable to create activity node", e);
131 		// }
132 	}
133 
134 	@Override
135 	public boolean isKnownType(Node entity) {
136 		if (ConnectJcrUtils.isNodeType(entity, ActivitiesTypes.ACTIVITIES_TASK)
137 				|| ConnectJcrUtils.isNodeType(entity, ActivitiesTypes.ACTIVITIES_ACTIVITY))
138 			return true;
139 		else
140 			return false;
141 	}
142 
143 	private static final String[] KNOWN_MIXIN = { ActivitiesTypes.ACTIVITIES_TASK, ActivitiesTypes.ACTIVITIES_NOTE,
144 			ActivitiesTypes.ACTIVITIES_MEETING, ActivitiesTypes.ACTIVITIES_SENT_EMAIL,
145 			ActivitiesTypes.ACTIVITIES_SENT_FAX, ActivitiesTypes.ACTIVITIES_SENT_EMAIL,
146 			ActivitiesTypes.ACTIVITIES_BLOG_POST, ActivitiesTypes.ACTIVITIES_CALL, ActivitiesTypes.ACTIVITIES_CHAT,
147 			ActivitiesTypes.ACTIVITIES_PAYMENT, ActivitiesTypes.ACTIVITIES_POLL, ActivitiesTypes.ACTIVITIES_RATE,
148 			ActivitiesTypes.ACTIVITIES_REVIEW, ActivitiesTypes.ACTIVITIES_TWEET, ActivitiesTypes.ACTIVITIES_ACTIVITY };
149 
150 	@Override
151 	public String getMainNodeType(Node entity) {
152 
153 		for (String mixin : KNOWN_MIXIN)
154 			if (ConnectJcrUtils.isNodeType(entity, mixin))
155 				return mixin;
156 		return null;
157 	}
158 
159 	@Override
160 	public boolean isKnownType(String nodeType) {
161 		for (String mixin : KNOWN_MIXIN)
162 			if (mixin.equals(nodeType))
163 				return true;
164 		return false;
165 	}
166 
167 	/* ACTIVITIES APP SPECIFIC METHODS */
168 
169 	private String dateAsRelPath(Calendar cal) {
170 		// StringBuffer buf = new StringBuffer(9);
171 		// buf.append('Y');
172 		// buf.append(cal.get(Calendar.YEAR));
173 		// buf.append('/');
174 		//
175 		// int woy = cal.get(Calendar.WEEK_OF_YEAR);
176 		// buf.append('W');
177 		// if (woy < 10)
178 		// buf.append(0);
179 		// buf.append(woy);
180 		// return buf.toString();
181 		//
182 		return JcrUtils.dateAsPath(cal, true);
183 	}
184 
185 	/* ACTIVITIES */
186 
187 	@Override
188 	public Calendar getActivityRelevantDate(Node activityNode) {
189 		try {
190 			Calendar relevantDate = null;
191 			if (activityNode.isNodeType(ActivitiesTypes.ACTIVITIES_TASK)) {
192 				if (activityNode.hasProperty(ConnectNames.CONNECT_CLOSE_DATE))
193 					relevantDate = activityNode.getProperty(ConnectNames.CONNECT_CLOSE_DATE).getDate();
194 				else if (activityNode.hasProperty(ActivitiesNames.ACTIVITIES_DUE_DATE))
195 					relevantDate = activityNode.getProperty(ActivitiesNames.ACTIVITIES_DUE_DATE).getDate();
196 				else if (activityNode.hasProperty(ActivitiesNames.ACTIVITIES_WAKE_UP_DATE))
197 					relevantDate = activityNode.getProperty(ActivitiesNames.ACTIVITIES_WAKE_UP_DATE).getDate();
198 				else if (activityNode.hasProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE))
199 					relevantDate = activityNode.getProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE).getDate();
200 				else if (activityNode.hasProperty(Property.JCR_LAST_MODIFIED))
201 					relevantDate = activityNode.getProperty(Property.JCR_LAST_MODIFIED).getDate();
202 				else if (activityNode.hasProperty(Property.JCR_CREATED))
203 					relevantDate = activityNode.getProperty(Property.JCR_CREATED).getDate();
204 			} else if (activityNode.isNodeType(ActivitiesTypes.ACTIVITIES_ACTIVITY)) {
205 				if (activityNode.hasProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE))
206 					relevantDate = activityNode.getProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE).getDate();
207 				else if (activityNode.hasProperty(Property.JCR_LAST_MODIFIED))
208 					relevantDate = activityNode.getProperty(Property.JCR_LAST_MODIFIED).getDate();
209 				else if (activityNode.hasProperty(Property.JCR_CREATED))
210 					relevantDate = activityNode.getProperty(Property.JCR_CREATED).getDate();
211 			}
212 			return relevantDate;
213 		} catch (RepositoryException re) {
214 			throw new ActivitiesException("Unable to get relevant date " + "for activity " + activityNode, re);
215 		}
216 	}
217 
218 	@Override
219 	public String getActivityLabel(Node activity) {
220 		try {
221 			for (String type : ActivityValueCatalogs.MAPS_ACTIVITY_TYPES.keySet()) {
222 				if (activity.isNodeType(type))
223 					return ActivityValueCatalogs.MAPS_ACTIVITY_TYPES.get(type);
224 			}
225 			throw new ActivitiesException("Undefined type for activity: " + activity);
226 		} catch (RepositoryException e) {
227 			throw new ActivitiesException("Unable to get type for activity " + activity, e);
228 		}
229 	}
230 
231 	/* TASKS */
232 	@Override
233 	public NodeIterator getMyTasks(Session session, boolean onlyOpenTasks) {
234 		List<String> normalisedRoles = new ArrayList<>();
235 		for (String role : CurrentUser.roles())
236 			normalisedRoles.add(normalizeDn(role));
237 		String[] nrArr = normalisedRoles.toArray(new String[0]);
238 		return getTasksForGroup(session, nrArr, onlyOpenTasks);
239 		// return getTasksForUser(session, session.getUserID(), onlyOpenTasks);
240 	}
241 
242 	private String normalizeDn(String dn) {
243 		// FIXME dirty workaround for the DN key case issue
244 		String lowerCased = dn.replaceAll("UID=", "uid=").replaceAll("CN=", "cn=").replaceAll("DC=", "dc=")
245 				.replaceAll("OU=", "ou=").replaceAll(", ", ",");
246 		return lowerCased;
247 	}
248 
249 	/**
250 	 * 
251 	 * @param session
252 	 * @param username
253 	 * @param onlyOpenTasks
254 	 * @return an empty list if none were found
255 	 */
256 	@Override
257 	public NodeIterator getTasksForUser(Session session, String username, boolean onlyOpenTasks) {
258 		return getTasksForGroup(session, userAdminService.getUserRoles(username), onlyOpenTasks);
259 	}
260 
261 	public NodeIterator getTasksForGroup(Session session, String[] roles, boolean onlyOpenTasks) {
262 		try {
263 			// XPath
264 			StringBuilder builder = new StringBuilder();
265 			builder.append("//element(*, ").append(ActivitiesTypes.ACTIVITIES_TASK).append(")");
266 
267 			// Assigned to
268 			StringBuilder tmpBuilder = new StringBuilder();
269 			for (String role : roles) {
270 				String attrQuery = XPathUtils.getPropertyEquals(ActivitiesNames.ACTIVITIES_ASSIGNED_TO, role);
271 				if (ConnectUtils.notEmpty(attrQuery))
272 					tmpBuilder.append(attrQuery).append(" or ");
273 			}
274 			String groupCond = null;
275 			if (tmpBuilder.length() > 4)
276 				groupCond = "(" + tmpBuilder.substring(0, tmpBuilder.length() - 3) + ")";
277 
278 			// Only opened tasks
279 			String notClosedCond = null;
280 			if (onlyOpenTasks)
281 				notClosedCond = "not(@" + ConnectNames.CONNECT_CLOSE_DATE + ")";
282 
283 			String allCond = XPathUtils.localAnd(groupCond, notClosedCond);
284 			if (EclipseUiUtils.notEmpty(allCond))
285 				builder.append("[").append(allCond).append("]");
286 
287 			builder.append(" order by @").append(ActivitiesNames.ACTIVITIES_DUE_DATE).append(", @")
288 					.append(Property.JCR_LAST_MODIFIED).append(" ascending");
289 			if (log.isTraceEnabled())
290 				log.trace("Getting todo list for " + CurrentUser.getDisplayName() + " (DN: " + CurrentUser.getUsername()
291 						+ ") with query: " + builder.toString());
292 			Query query = XPathUtils.createQuery(session, builder.toString());
293 			return query.execute().getNodes();
294 		} catch (RepositoryException e) {
295 			throw new ActivitiesException("Unable to get tasks for groups " + roles.toString());
296 		}
297 	}
298 
299 	protected boolean manageClosedState(String templateId, Node taskNode, String oldStatus, String newStatus,
300 			List<String> modifiedPaths) throws RepositoryException {
301 		try {
302 			Session session = taskNode.getSession();
303 			List<String> closingStatus = resourcesService.getTemplateCatalogue(session, templateId,
304 					ActivitiesNames.ACTIVITIES_TASK_CLOSING_STATUSES, null);
305 
306 			boolean changed = false;
307 
308 			if (closingStatus.contains(newStatus)) {
309 				if (closingStatus.contains(oldStatus)) {
310 					// Already closed, nothing to do
311 				} else {
312 					// Close
313 					taskNode.setProperty(ConnectNames.CONNECT_CLOSE_DATE, new GregorianCalendar());
314 					taskNode.setProperty(ConnectNames.CONNECT_CLOSED_BY, session.getUserID());
315 					changed = true;
316 				}
317 			} else {
318 				if (!closingStatus.contains(oldStatus)) {
319 					// Already open, nothing to do
320 				} else {
321 					// Re-Open
322 					if (taskNode.hasProperty(ConnectNames.CONNECT_CLOSE_DATE))
323 						taskNode.getProperty(ConnectNames.CONNECT_CLOSE_DATE).remove();
324 					if (taskNode.hasProperty(ConnectNames.CONNECT_CLOSED_BY))
325 						taskNode.getProperty(ConnectNames.CONNECT_CLOSED_BY).remove();
326 					changed = true;
327 				}
328 			}
329 			return changed;
330 		} catch (RepositoryException re) {
331 			throw new RepositoryException("Unable to manage closed state for " + newStatus + " status for task "
332 					+ taskNode + " of template ID " + templateId, re);
333 		}
334 	}
335 
336 	public boolean updateStatus(String templateId, Node taskNode, String newStatus, List<String> modifiedPaths)
337 			throws RepositoryException {
338 		try {
339 			String oldStatus = ConnectJcrUtils.get(taskNode, ActivitiesNames.ACTIVITIES_TASK_STATUS);
340 			if (ConnectUtils.notEmpty(oldStatus) && oldStatus.equals(newStatus))
341 				return false;
342 			else {
343 				taskNode.setProperty(ActivitiesNames.ACTIVITIES_TASK_STATUS, newStatus);
344 				manageClosedState(templateId, taskNode, oldStatus, newStatus, modifiedPaths);
345 				return true;
346 			}
347 		} catch (RepositoryException re) {
348 			throw new RepositoryException("Unable to set new status " + newStatus + " status for task " + taskNode
349 					+ " of template ID " + templateId, re);
350 		}
351 	}
352 
353 	@Override
354 	public boolean isTaskDone(Node taskNode) {
355 		try {
356 			// Only rely on the non-nullity of the closed date
357 			return taskNode.hasProperty(ConnectNames.CONNECT_CLOSE_DATE);
358 		} catch (RepositoryException re) {
359 			throw new ActivitiesException("Unable to get done status for task " + taskNode, re);
360 		}
361 	}
362 
363 	@Override
364 	public boolean isTaskSleeping(Node taskNode) {
365 		try {
366 			if (taskNode.hasProperty(ActivitiesNames.ACTIVITIES_WAKE_UP_DATE)) {
367 				Calendar wuDate = taskNode.getProperty(ActivitiesNames.ACTIVITIES_WAKE_UP_DATE).getDate();
368 				Calendar now = new GregorianCalendar();
369 				// Add a day: the task is awake as from 00:01AM on the given day
370 				now.add(Calendar.DAY_OF_YEAR, 1);
371 				return wuDate.after(now);
372 			} else
373 				return false;
374 		} catch (RepositoryException re) {
375 			throw new ActivitiesException("Unable to get sleeping status for task " + taskNode, re);
376 		}
377 	}
378 
379 	/** Get the display name of the assigned to group for this task */
380 	@Override
381 	public String getAssignedToDisplayName(Node taskNode) {
382 		try {
383 			if (taskNode.hasProperty(ActivitiesNames.ACTIVITIES_ASSIGNED_TO)) {
384 				String groupId = taskNode.getProperty(ActivitiesNames.ACTIVITIES_ASSIGNED_TO).getString();
385 				return userAdminService.getUserDisplayName(groupId);
386 			}
387 			return "";
388 		} catch (RepositoryException e) {
389 			throw new ActivitiesException("Unable to get name of group assigned to " + taskNode, e);
390 		}
391 	}
392 
393 	@Override
394 	public Node configureTask(Node task, String taskNodeType, String title, String description, String assignedTo)
395 			throws RepositoryException {
396 		return configureTask(task, taskNodeType, task.getSession().getUserID(), title, description, assignedTo, null,
397 				new GregorianCalendar(), null, null);
398 	}
399 	//
400 	// @Override
401 	// public Node createDraftTask(Session session, String title, String
402 	// description, String assignedTo,
403 	// List<Node> relatedTo, Calendar dueDate, Calendar wakeUpDate) throws
404 	// RepositoryException {
405 	// return createDraftTask(session, CurrentUser.getUsername(), title,
406 	// description, assignedTo, relatedTo,
407 	// new GregorianCalendar(), dueDate, wakeUpDate);
408 	// }
409 	//
410 	// @Override
411 	// public Node createDraftTask(Session session, String reporterId, String
412 	// title, String description, String assignedTo,
413 	// List<Node> relatedTo, Calendar creationDate, Calendar dueDate, Calendar
414 	// wakeUpDate)
415 	// throws RepositoryException {
416 	// return createDraftTask(session, ActivitiesTypes.ACTIVITIES_TASK,
417 	// reporterId, title, description, assignedTo,
418 	// relatedTo, creationDate, dueDate, wakeUpDate);
419 	// }
420 
421 	@Override
422 	public Node configureTask(Node draftTask, String taskNodeType, String reporterId, String title, String description,
423 			String assignedTo, List<Node> relatedTo, Calendar creationDate, Calendar dueDate, Calendar wakeUpDate)
424 			throws RepositoryException {
425 
426 		// Node parentNode = getDraftParent(session);
427 		// // We use the mixin as node name
428 		// Node taskNode = parentNode.addNode(taskNodeType);
429 		// taskNode.addMixin(taskNodeType);
430 
431 		// Node draftTask = createDraftEntity(session, taskNodeType);
432 
433 		if (ConnectUtils.notEmpty(title))
434 			draftTask.setProperty(Property.JCR_TITLE, title);
435 		if (ConnectUtils.notEmpty(description))
436 			draftTask.setProperty(Property.JCR_DESCRIPTION, description);
437 		if (EclipseUiUtils.isEmpty(reporterId))
438 			reporterId = draftTask.getSession().getUserID();
439 		draftTask.setProperty(ActivitiesNames.ACTIVITIES_REPORTED_BY, reporterId);
440 
441 		if (ConnectUtils.notEmpty(assignedTo))
442 			draftTask.setProperty(ActivitiesNames.ACTIVITIES_ASSIGNED_TO, assignedTo);
443 
444 		if (relatedTo != null && !relatedTo.isEmpty())
445 			ConnectJcrUtils.setMultipleReferences(draftTask, ActivitiesNames.ACTIVITIES_RELATED_TO, relatedTo);
446 
447 		if (creationDate == null)
448 			creationDate = new GregorianCalendar();
449 		draftTask.setProperty(ActivitiesNames.ACTIVITIES_ACTIVITY_DATE, creationDate);
450 
451 		if (dueDate != null) {
452 			draftTask.setProperty(ActivitiesNames.ACTIVITIES_DUE_DATE, dueDate);
453 		}
454 		if (wakeUpDate != null) {
455 			draftTask.setProperty(ActivitiesNames.ACTIVITIES_WAKE_UP_DATE, wakeUpDate);
456 		}
457 		setTaskDefaultStatus(draftTask, taskNodeType);
458 		return draftTask;
459 	}
460 
461 	@Override
462 	public void setTaskDefaultStatus(Node taskNode, String taskNodeType) throws RepositoryException {
463 		// Default status management
464 		Node template = resourcesService.getNodeTemplate(taskNode.getSession(), taskNodeType);
465 		String defaultStatus = null;
466 		if (template != null)
467 			defaultStatus = ConnectJcrUtils.get(template, ActivitiesNames.ACTIVITIES_TASK_DEFAULT_STATUS);
468 		if (ConnectUtils.notEmpty(defaultStatus))
469 			taskNode.setProperty(ActivitiesNames.ACTIVITIES_TASK_STATUS, defaultStatus);
470 	}
471 
472 	// @Override
473 	// public Node createPoll(Node parentNode, String reporterId, String
474 	// pollName, String title, String description,
475 	// String assignedTo, List<Node> relatedTo, Calendar creationDate, Calendar
476 	// dueDate, Calendar wakeUpDate)
477 	// throws RepositoryException {
478 	// Node poll = createDraftTask(null, ActivitiesTypes.ACTIVITIES_POLL,
479 	// reporterId, title, description, assignedTo,
480 	// relatedTo, creationDate, dueDate, wakeUpDate);
481 	//
482 	// String newPath = parentNode.getPath() + "/" +
483 	// JcrUtils.replaceInvalidChars(pollName);
484 	// poll.setProperty(ActivitiesNames.ACTIVITIES_POLL_NAME, pollName);
485 	// poll.addNode(ActivitiesNames.ACTIVITIES_RATES);
486 	//
487 	// // TODO clean this
488 	// Session session = parentNode.getSession();
489 	// session.move(poll.getPath(), newPath);
490 	// // } catch (RepositoryException e) {
491 	// // throw new ActivitiesException(
492 	// // "Unable to add poll specific info to task " + poll + " and move it to
493 	// // " + newPath, e);
494 	// // }
495 	// return poll;
496 	// }
497 
498 	/* DEPENDENCY INJECTION */
499 	public void setUserAdminService(UserAdminService userAdminService) {
500 		this.userAdminService = userAdminService;
501 	}
502 
503 	public void setResourcesService(ResourcesService resourcesService) {
504 		this.resourcesService = resourcesService;
505 	}
506 }