Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.activities.post.lookup; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.alfresco.model.ContentModel; import org.alfresco.repo.activities.ActivityType; import org.alfresco.repo.domain.activities.ActivityPostDAO; import org.alfresco.repo.domain.activities.ActivityPostEntity; import org.alfresco.repo.lock.JobLockService; import org.alfresco.repo.lock.JobLockService.JobLockRefreshCallback; import org.alfresco.repo.lock.LockAcquisitionException; import org.alfresco.repo.tenant.TenantService; import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.site.SiteService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.util.Pair; import org.alfresco.util.PathUtil; import org.alfresco.util.PropertyCheck; import org.alfresco.util.VmShutdownListener; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.quartz.JobExecutionException; /** * The post lookup component is responsible for updating posts that require a secondary lookup (to get additional activity data) * * @author janv * @since 3.0 */ public class PostLookup { private static Log logger = LogFactory.getLog(PostLookup.class); private static VmShutdownListener vmShutdownListener = new VmShutdownListener(PostLookup.class.getName()); /** The name of the lock used to ensure that post lookup does not run on more than one node at the same time */ private static final QName LOCK_QNAME = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "ActivityPostLookup"); /** The time this lock will persist in the database (60 sec but refreshed at regular intervals) */ private static final long LOCK_TTL = 1000 * 60; private ActivityPostDAO postDAO; private NodeService nodeService; private PermissionService permissionService; private TransactionService transactionService; private PersonService personService; private TenantService tenantService; private SiteService siteService; private JobLockService jobLockService; public static final String JSON_NODEREF_LOOKUP = "nodeRefL"; // requires additional lookup public static final String JSON_NODEREF = "nodeRef"; public static final String JSON_NODEREF_PARENT = "parentNodeRef"; public static final String JSON_FIRSTNAME = "firstName"; public static final String JSON_LASTNAME = "lastName"; public static final String JSON_NAME = "name"; public static final String JSON_TYPEQNAME = "typeQName"; public static final String JSON_PARENT_NODEREF = "parentNodeRef"; public static final String JSON_DISPLAY_PATH = "displayPath"; public static final String JSON_TENANT_DOMAIN = "tenantDomain"; // for Share public static final String JSON_TITLE = "title"; public static final String JSON_PAGE = "page"; private static Map<String, String> rollupTypes = new HashMap<String, String>(3); // note: consistent with Share 'groupActivitiesAt' private int rollupCount = 5; private int maxItemsPerCycle = 500; public void setPostDAO(ActivityPostDAO postDAO) { this.postDAO = postDAO; } public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } public void setTransactionService(TransactionService transactionService) { this.transactionService = transactionService; } public void setPersonService(PersonService personService) { this.personService = personService; } public void setTenantService(TenantService tenantService) { this.tenantService = tenantService; } public void setSiteService(SiteService siteService) { this.siteService = siteService; } public void setRollupCount(int rollupCount) { this.rollupCount = rollupCount; } public void setJobLockService(JobLockService jobLockService) { this.jobLockService = jobLockService; } public void setMaxItemsPerCycle(int maxItemsPerCycle) { this.maxItemsPerCycle = maxItemsPerCycle; } /** * Perform basic checks to ensure that the necessary dependencies were injected. */ private void checkProperties() { PropertyCheck.mandatory(this, "postDAO", postDAO); PropertyCheck.mandatory(this, "nodeService", nodeService); PropertyCheck.mandatory(this, "permissionService", permissionService); PropertyCheck.mandatory(this, "transactionService", transactionService); PropertyCheck.mandatory(this, "personService", personService); PropertyCheck.mandatory(this, "tenantService", tenantService); rollupTypes.put(ActivityType.FILE_ADDED, ActivityType.FILES_ADDED); rollupTypes.put(ActivityType.FILE_UPDATED, ActivityType.FILES_UPDATED); rollupTypes.put(ActivityType.FILE_DELETED, ActivityType.FILES_DELETED); rollupTypes.put(ActivityType.FOLDER_ADDED, ActivityType.FOLDERS_ADDED); rollupTypes.put(ActivityType.FOLDER_DELETED, ActivityType.FOLDERS_DELETED); } public void execute() throws JobExecutionException { checkProperties(); // Avoid running when in read-only mode if (!transactionService.getAllowWrite()) { if (logger.isTraceEnabled()) { logger.trace("Post lookup not running due to read-only server"); } return; } long start = System.currentTimeMillis(); String lockToken = null; LockCallback lockCallback = new LockCallback(); try { if (jobLockService != null) { lockToken = acquireLock(lockCallback); } ActivityPostEntity params = new ActivityPostEntity(); params.setStatus(ActivityPostEntity.STATUS.PENDING.toString()); if (logger.isDebugEnabled()) { logger.debug( "Selecting activity posts with status: " + ActivityPostEntity.STATUS.PENDING.toString()); } // get all pending post (for this job run) final List<ActivityPostEntity> activityPosts = postDAO.selectPosts(params, maxItemsPerCycle); if (activityPosts.size() > 0) { if (logger.isDebugEnabled()) { logger.debug("Update: " + activityPosts.size() + " activity post" + (activityPosts.size() == 1 ? "s" : "")); } // execute in READ txn transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<Object>() { public Object execute() throws Throwable { // lookup any additional data lookupPosts(activityPosts); return null; } }, true); // execute in WRITE txn List<ActivityPostEntity> activityPostsToUpdate = transactionService.getRetryingTransactionHelper() .doInTransaction(new RetryingTransactionCallback<List<ActivityPostEntity>>() { public List<ActivityPostEntity> execute() throws Throwable { // collapse (ie. rollup) and relevant posts return rollupPosts(activityPosts); } }, false); // update posts + status (note: will also add any new rolled-up posts) updatePosts(activityPostsToUpdate); if (logger.isInfoEnabled()) { int cnt = activityPostsToUpdate.size(); logger.info("Updated: " + cnt + " activity post" + (cnt == 1 ? "" : "s") + " (in " + (System.currentTimeMillis() - start) + " msecs)"); } } } catch (LockAcquisitionException e) { // Job being done by another process if (logger.isDebugEnabled()) { logger.debug("execute: Can't get lock. Assume post lookup job already underway: " + e); } } catch (SQLException e) { logger.error("Exception during select of posts: ", e); throw new JobExecutionException(e); } catch (Throwable e) { // If the VM is shutting down, then ignore if (vmShutdownListener.isVmShuttingDown()) { // Ignore } else { logger.error("Exception during update of posts: ", e); } } finally { releaseLock(lockCallback, lockToken); } } private class UserRollupActivity { private String userId; private String activityType; private NodeRef parentNodeRef; public UserRollupActivity(String userId, String activityType, NodeRef parentNodeRef) { this.userId = userId; this.activityType = activityType; this.parentNodeRef = parentNodeRef; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((userId == null) ? 0 : userId.hashCode()); result = prime * result + ((activityType == null) ? 0 : activityType.hashCode()); result = prime * result + ((parentNodeRef == null) ? 0 : parentNodeRef.hashCode()); return result; } @Override public boolean equals(Object obj) { if (obj == null) return false; if (!(obj instanceof UserRollupActivity)) return false; UserRollupActivity that = (UserRollupActivity) obj; return this.userId.equals(that.userId) && this.activityType.equals(that.activityType) && this.parentNodeRef.equals(that.parentNodeRef); } } private List<ActivityPostEntity> lookupPosts(final List<ActivityPostEntity> activityPosts) { for (final ActivityPostEntity activityPost : activityPosts) { if (logger.isDebugEnabled()) { logger.debug("Selected activity post: " + activityPost); } final String postUserId = activityPost.getUserId(); try { // MT share String tenantDomain = TenantService.DEFAULT_DOMAIN; final JSONObject jo = new JSONObject(new JSONTokener(activityPost.getActivityData())); if (!jo.isNull(JSON_TENANT_DOMAIN)) { tenantDomain = jo.getString(JSON_TENANT_DOMAIN); } activityPost.setTenantDomain(tenantDomain); TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork<Void>() { public Void doWork() throws Exception { JSONObject joLookup = null; if (!jo.isNull(JSON_NODEREF_LOOKUP)) { String nodeRefStr = jo.getString(JSON_NODEREF_LOOKUP); NodeRef nodeRef = new NodeRef(nodeRefStr); // lookup additional node data joLookup = lookupNode(nodeRef, postUserId, jo); } else { // lookup poster's firstname/lastname (if needed) if ((jo.isNull(JSON_FIRSTNAME)) || (jo.isNull(JSON_LASTNAME))) { Pair<String, String> firstLastName = lookupPerson(postUserId); if (firstLastName != null) { jo.put(JSON_FIRSTNAME, firstLastName.getFirst()); jo.put(JSON_LASTNAME, firstLastName.getSecond()); joLookup = jo; } } // lookup parent nodeRef (if needed) NodeRef parentNodeRef = activityPost.getParentNodeRef(); if (parentNodeRef == null) { String parentNodeRefStr = null; if (jo.isNull(JSON_PARENT_NODEREF)) { if (!jo.isNull(JSON_NODEREF)) { parentNodeRef = lookupParentNodeRef( new NodeRef(jo.getString(JSON_NODEREF))); if (parentNodeRef != null) { parentNodeRefStr = parentNodeRef.toString(); jo.put(JSON_PARENT_NODEREF, parentNodeRefStr); // note: currently only required during lookup/rollup //joLookup = jo; } } } else { parentNodeRefStr = jo.getString(JSON_PARENT_NODEREF); } if (parentNodeRefStr != null) { activityPost.setParentNodeRef(new NodeRef(parentNodeRefStr)); } } // lookup site (if needed) String siteId = activityPost.getSiteNetwork(); if (siteId == null) { if (!jo.isNull(JSON_NODEREF)) { String nodeRefStr = jo.getString(JSON_NODEREF); if (nodeRefStr != null) { siteId = lookupSite(new NodeRef(nodeRefStr)); activityPost.setSiteNetwork(siteId); } } } } if (joLookup != null) { // extra data was looked-up activityPost.setActivityData(joLookup.toString()); } if ((activityPost.getActivityData() != null) && (activityPost.getActivityData() .length() > ActivityPostDAO.MAX_LEN_ACTIVITY_DATA)) { throw new IllegalArgumentException( "Invalid activity data - exceeds " + ActivityPostDAO.MAX_LEN_ACTIVITY_DATA + " chars: " + activityPost.getActivityData()); } if ((activityPost.getSiteNetwork() != null) && (activityPost.getSiteNetwork().length() > ActivityPostDAO.MAX_LEN_SITE_ID)) { // belts-and-braces - should not get here since checked during post (and not modified) throw new IllegalArgumentException("Invalid siteId - exceeds " + ActivityPostDAO.MAX_LEN_SITE_ID + " chars: " + activityPost.getSiteNetwork()); } activityPost.setLastModified(new Date()); return null; } }, tenantDomain); } catch (Exception e) { // log error, but consume exception (skip this post) logger.error("Skipping activity post " + activityPost.getId() + ": " + e); activityPost.setStatus(ActivityPostEntity.STATUS.ERROR.toString()); } } return activityPosts; } private List<ActivityPostEntity> rollupPosts(List<ActivityPostEntity> activityPosts) throws SQLException { Map<UserRollupActivity, List<ActivityPostEntity>> rollupPosts = new HashMap<UserRollupActivity, List<ActivityPostEntity>>(); List<ActivityPostEntity> result = new ArrayList<ActivityPostEntity>(activityPosts.size()); for (final ActivityPostEntity post : activityPosts) { if (rollupTypes.containsKey(post.getActivityType()) && (post.getParentNodeRef() != null)) { UserRollupActivity key = new UserRollupActivity(post.getUserId(), post.getActivityType(), post.getParentNodeRef()); List<ActivityPostEntity> posts = rollupPosts.get(key); if (posts == null) { posts = new ArrayList<ActivityPostEntity>(); rollupPosts.put(key, posts); } posts.add(post); } else { result.add(post); } } for (final Map.Entry<UserRollupActivity, List<ActivityPostEntity>> entry : rollupPosts.entrySet()) { final int count = entry.getValue().size(); if (count >= rollupCount) { final ActivityPostEntity oldPost = entry.getValue().get(0); final String tenantDomain = oldPost.getTenantDomain(); TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork<Void>() { public Void doWork() throws Exception { String postUserId = oldPost.getUserId(); // rollup - create a new 'posted' event that represents the rolled-up activity (and set others to 'processed') ActivityPostEntity newPost = new ActivityPostEntity(); newPost.setActivityType(rollupTypes.get(oldPost.getActivityType())); newPost.setPostDate(oldPost.getPostDate()); newPost.setUserId(postUserId); newPost.setSiteNetwork(oldPost.getSiteNetwork()); newPost.setAppTool(oldPost.getAppTool()); newPost.setLastModified(oldPost.getLastModified()); newPost.setTenantDomain(tenantDomain); newPost.setJobTaskNode(1); try { JSONObject jo = new JSONObject(); jo.put(JSON_NODEREF_PARENT, oldPost.getParentNodeRef().toString()); jo.put(JSON_TENANT_DOMAIN, tenantDomain); jo.put(JSON_TITLE, "" + count); Pair<String, String> firstLastName = lookupPerson(postUserId); if (firstLastName != null) { jo.put(JSON_FIRSTNAME, firstLastName.getFirst()); jo.put(JSON_LASTNAME, firstLastName.getSecond()); } Path path = lookupPath(oldPost.getParentNodeRef()); if (path != null) { String displayPath = PathUtil.getDisplayPath(path, true); if (displayPath != null) { // note: PathUtil.getDisplayPath returns prefix path as: '/company_home/sites/' rather than /Company Home/Sites' String prefix = "/company_home/sites/" + tenantService.getBaseName(oldPost.getSiteNetwork()) + "/documentLibrary"; int idx = displayPath.indexOf(prefix); if (idx == 0) { displayPath = displayPath.substring(prefix.length()); } // Share-specific jo.put(JSON_PAGE, "documentlibrary?path=" + displayPath); } } newPost.setActivityData(jo.toString()); newPost.setStatus(ActivityPostEntity.STATUS.POSTED.toString()); } catch (JSONException e) { logger.warn("Unable to create activity data: " + e); newPost.setStatus(ActivityPostEntity.STATUS.ERROR.toString()); } for (ActivityPostEntity post : entry.getValue()) { post.setStatus(ActivityPostEntity.STATUS.PROCESSED.toString()); } // add the new POSTED entry.getValue().add(newPost); return null; } }, tenantDomain); } result.addAll(entry.getValue()); } return result; } private void updatePosts(List<ActivityPostEntity> activityPosts) throws SQLException { for (final ActivityPostEntity activityPost : activityPosts) { // MT share final String tenantDomain = activityPost.getTenantDomain(); TenantUtil.runAsSystemTenant(new TenantUtil.TenantRunAsWork<Object>() { public Object doWork() throws Exception { try { postDAO.startTransaction(); ActivityPostEntity.STATUS status = ActivityPostEntity.STATUS .valueOf(activityPost.getStatus()); switch (status) { case ERROR: case PROCESSED: postDAO.updatePostStatus(activityPost.getId(), status); break; case POSTED: if (activityPost.getId() == null) { // eg. rolled-up post postDAO.insertPost(activityPost); } break; case PENDING: postDAO.updatePost(activityPost.getId(), activityPost.getSiteNetwork(), activityPost.getActivityData(), ActivityPostEntity.STATUS.POSTED); activityPost.setStatus(ActivityPostEntity.STATUS.POSTED.toString()); // for debug output break; default: throw new Exception("Unexpected status: " + status); } if (logger.isDebugEnabled()) { logger.debug("Updated: " + activityPost); } postDAO.commitTransaction(); } catch (SQLException e) { logger.error("Exception during update of post: ", e); throw new JobExecutionException(e); } catch (Exception e) { // log error, but consume exception (skip this post) logger.error("Skipping activity post " + activityPost.getId() + ": " + e); postDAO.updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); postDAO.commitTransaction(); } finally { postDAO.endTransaction(); } return null; } }, tenantDomain); } } private Path lookupPath(final NodeRef nodeRef) { Path path = null; if ((nodeRef != null) && (nodeService.exists(nodeRef))) { path = nodeService.getPath(nodeRef); } return path; } private Pair<String, String> lookupPerson(final String postUserId) throws JSONException { Pair<String, String> result = null; if (personService.personExists(postUserId)) { NodeRef personRef = personService.getPerson(postUserId); String firstName = (String) nodeService.getProperty(personRef, ContentModel.PROP_FIRSTNAME); String lastName = (String) nodeService.getProperty(personRef, ContentModel.PROP_LASTNAME); result = new Pair<String, String>(firstName, lastName); } return result; } private NodeRef lookupParentNodeRef(final NodeRef nodeRef) throws JSONException { NodeRef parentNodeRef = null; if (nodeService.exists(nodeRef)) { parentNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); } return parentNodeRef; } private String lookupSite(final NodeRef nodeRef) throws JSONException { String siteId = null; if (nodeService.exists(nodeRef)) { siteId = siteService.getSiteShortName(nodeRef); } return siteId; } /** * Generic node lookup - note: not currently used (see ActivityService.postActivity when activityData is not supplied) */ private JSONObject lookupNode(final NodeRef nodeRef, final String postUserId, final JSONObject jo) throws JSONException { String name = ""; if (!jo.isNull(JSON_NAME)) { name = jo.getString(JSON_NAME); } NodeRef parentNodeRef = null; if (!jo.isNull(JSON_PARENT_NODEREF)) { parentNodeRef = new NodeRef(jo.getString(JSON_PARENT_NODEREF)); } String typeQName = ""; if (!jo.isNull(JSON_TYPEQNAME)) { typeQName = jo.getString(JSON_TYPEQNAME); } String displayPath = ""; Path path = null; String firstName = ""; String lastName = ""; if (personService.personExists(postUserId)) { // lookup firstname, lastname NodeRef personRef = personService.getPerson(postUserId); firstName = (String) nodeService.getProperty(personRef, ContentModel.PROP_FIRSTNAME); lastName = (String) nodeService.getProperty(personRef, ContentModel.PROP_LASTNAME); } if ((nodeRef != null) && (nodeService.exists(nodeRef))) { if (name.length() == 0) { // lookup node name name = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); } if (typeQName.length() == 0) { // lookup type typeQName = nodeService.getType(nodeRef).toPrefixString(); // TODO: missing the prefix ? } if (parentNodeRef == null) { // lookup parent node parentNodeRef = nodeService.getPrimaryParent(nodeRef).getParentRef(); } } if (parentNodeRef != null) { // lookup parent node path (if node exists) path = lookupPath(parentNodeRef); } if (path != null) { // lookup display path displayPath = path.toDisplayPath(nodeService, permissionService); // note: for now, also tack on the node name displayPath += "/" + name; } // merge with existing activity data jo.put(JSON_NAME, name); jo.put(JSON_NODEREF, nodeRef.toString()); jo.put(JSON_TYPEQNAME, typeQName); jo.put(JSON_PARENT_NODEREF, (parentNodeRef != null ? parentNodeRef.toString() : null)); jo.put(JSON_DISPLAY_PATH, displayPath); jo.put(JSON_FIRSTNAME, firstName); jo.put(JSON_LASTNAME, lastName); return jo; } private class LockCallback implements JobLockRefreshCallback { final AtomicBoolean running = new AtomicBoolean(true); @Override public boolean isActive() { return running.get(); } @Override public synchronized void lockReleased() { if (logger.isDebugEnabled()) { logger.debug("Lock release notification: " + LOCK_QNAME); } running.set(false); } } private synchronized String acquireLock(LockCallback lockCallback) throws LockAcquisitionException { // Try to get lock String lockToken = jobLockService.getLock(LOCK_QNAME, LOCK_TTL); // Got the lock - now register the refresh callback which will keep the lock alive jobLockService.refreshLock(lockToken, LOCK_QNAME, LOCK_TTL, lockCallback); if (logger.isDebugEnabled()) { logger.debug("Lock acquired: " + LOCK_QNAME + ": " + lockToken); } return lockToken; } private synchronized void releaseLock(LockCallback lockCallback, String lockToken) { try { if (lockCallback != null) { lockCallback.running.set(false); } if (lockToken != null) { jobLockService.releaseLock(lockToken, LOCK_QNAME); if (logger.isDebugEnabled()) { logger.debug("Lock released: " + LOCK_QNAME + ": " + lockToken); } } } catch (LockAcquisitionException e) { // Ignore if (logger.isDebugEnabled()) { logger.debug("Lock release failed: " + LOCK_QNAME + ": " + lockToken + "(" + e.getMessage() + ")"); } } } }