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.feed; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.alfresco.repo.activities.post.lookup.PostLookup; import org.alfresco.repo.domain.activities.ActivitiesDAO; import org.alfresco.repo.domain.activities.ActivityFeedDAO; import org.alfresco.repo.domain.activities.ActivityFeedEntity; import org.alfresco.repo.domain.activities.ActivityPostEntity; import org.alfresco.repo.domain.activities.FeedControlEntity; import org.alfresco.repo.template.ISO8601DateFormatMethod; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.util.JSONtoFmModel; import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.springframework.extensions.surf.util.Base64; import freemarker.cache.URLTemplateLoader; import freemarker.core.TemplateClassResolver; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.Version; /** * Responsible for processing the individual task */ public abstract class FeedTaskProcessor { private static final Log logger = LogFactory.getLog(FeedTaskProcessor.class); public static final String FEED_FORMAT_JSON = "json"; public static final String FEED_FORMAT_ATOMENTRY = "atomentry"; public static final String FEED_FORMAT_HTML = "html"; public static final String FEED_FORMAT_RSS = "rss"; public static final String FEED_FORMAT_TEXT = "text"; public static final String FEED_FORMAT_XML = "xml"; private static final String URL_SERVICE_SITES = "/api/sites"; private static final String URL_MEMBERSHIPS = "/memberships"; private static final String URL_SERVICE_TEMPLATES = "/api/activities/templates"; private static final String URL_SERVICE_TEMPLATE = "/api/activities/template"; private boolean userNamesAreCaseSensitive = false; public void setUserNamesAreCaseSensitive(boolean userNamesAreCaseSensitive) { this.userNamesAreCaseSensitive = userNamesAreCaseSensitive; } public void process(int jobTaskNode, long minSeq, long maxSeq, RepoCtx ctx) throws Exception { long startTime = System.currentTimeMillis(); if (logger.isDebugEnabled()) { logger.debug("Process: jobTaskNode '" + jobTaskNode + "' from seq '" + minSeq + "' to seq '" + maxSeq + "' on this node from grid job."); } ActivityPostEntity selector = new ActivityPostEntity(); selector.setJobTaskNode(jobTaskNode); selector.setMinId(minSeq); selector.setMaxId(maxSeq); selector.setStatus(ActivityPostEntity.STATUS.POSTED.toString()); List<ActivityPostEntity> activityPosts = null; int totalGenerated = 0; try { activityPosts = selectPosts(selector); if (logger.isDebugEnabled()) { logger.debug("Process: " + activityPosts.size() + " activity posts"); } // local caches for this run of activity posts Map<String, Set<String>> siteConnectedUsers = new HashMap<String, Set<String>>(); // site -> site members Map<Pair<String, String>, Set<String>> followerConnectedUsers = new HashMap<Pair<String, String>, Set<String>>(); // user -> followers Map<Pair<String, String>, Boolean> canUserReadSite = new HashMap<Pair<String, String>, Boolean>(); // <user, site> -> true/false (note: used when following, implied as true for site members) Map<String, List<FeedControlEntity>> userFeedControls = new HashMap<String, List<FeedControlEntity>>(); List<String> fmTemplates = Arrays.asList(new String[] { "activities/org/alfresco/generic.json.ftl" }); // for each activity post ... for (ActivityPostEntity activityPost : activityPosts) { String postingUserId = activityPost.getUserId(); String activityType = activityPost.getActivityType(); if (fmTemplates.size() == 0) { logger.error("Skipping activity post " + activityPost.getId() + " since no specific/generic templates for activityType: " + activityType); updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); continue; } Map<String, Object> model = null; try { model = JSONtoFmModel.convertJSONObjectToMap(activityPost.getActivityData()); } catch (JSONException je) { logger.error( "Skipping activity post " + activityPost.getId() + " due to invalid activity data: ", je); updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); continue; } String nodeRefStr = (String) model.get(PostLookup.JSON_NODEREF); try { // If a nodeRef is present, then it must be valid. if (nodeRefStr != null) { // Attempt to create a nodeRef, making use of the constructor's validation. new NodeRef(nodeRefStr); } } catch (Exception e) { logger.error("Skipping activity post " + activityPost.getId() + " due to invalid nodeRef: " + nodeRefStr); updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); continue; } // note: for MT share, site id should already be mangled - in addition to extra tenant domain info String thisSite = activityPost.getSiteNetwork(); String tenantDomain = (String) model.get(PostLookup.JSON_TENANT_DOMAIN); if (thisSite != null) { if (tenantDomain != null) { thisSite = getTenantName(thisSite, tenantDomain); } else { // for backwards compatibility tenantDomain = getTenantDomain(thisSite); } } if (tenantDomain == null) { tenantDomain = TenantService.DEFAULT_DOMAIN; } model.put(ActivityFeedEntity.KEY_ACTIVITY_FEED_TYPE, activityPost.getActivityType()); model.put(ActivityFeedEntity.KEY_ACTIVITY_FEED_SITE, thisSite); model.put("userId", activityPost.getUserId()); model.put("id", activityPost.getId()); model.put("date", activityPost.getPostDate()); // post date rather than time that feed is generated model.put("xmldate", new ISO8601DateFormatMethod()); model.put("repoEndPoint", ctx.getRepoEndPoint()); // Get recipients of this post Set<String> recipients = null; try { recipients = getRecipients(ctx, thisSite, activityPost.getUserId(), tenantDomain, siteConnectedUsers, followerConnectedUsers, canUserReadSite); } catch (Exception e) { logger.error( "Skipping activity post " + activityPost.getId() + " since failed to get recipients: ", e); updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); continue; } try { startTransaction(); if (logger.isTraceEnabled()) { logger.trace("Process: " + recipients.size() + " candidate connections for activity post " + activityPost.getId()); } int excludedConnections = 0; for (String recipient : recipients) { List<FeedControlEntity> feedControls = null; if (!recipient.equals("")) { // Get user's feed controls feedControls = userFeedControls.get(recipient); if (feedControls == null) { feedControls = getFeedControls(recipient); userFeedControls.put(recipient, feedControls); } } // filter based on opt-out feed controls (if any) if (!acceptActivity(activityPost, feedControls)) { excludedConnections++; } else { // node read permission check (if nodeRef is present) if (!canRead(ctx, recipient, model)) { excludedConnections++; continue; } for (String fmTemplate : fmTemplates) { String formatFound = FeedTaskProcessor.FEED_FORMAT_JSON; ActivityFeedEntity feed = new ActivityFeedEntity(); // Generate activity feed summary //MNT-9104 If username contains uppercase letters the action of joining a site will not be displayed in "My activities" if (!userNamesAreCaseSensitive) { recipient = recipient.toLowerCase(); postingUserId = postingUserId.toLowerCase(); } feed.setFeedUserId(recipient); feed.setPostUserId(postingUserId); feed.setActivityType(activityType); String activitySummary = null; // allows JSON to simply pass straight through activitySummary = activityPost.getActivityData(); if (!activitySummary.equals("")) { if (activitySummary.length() > ActivityFeedDAO.MAX_LEN_ACTIVITY_SUMMARY) { logger.warn("Skip feed entry (activity post " + activityPost.getId() + ") since activity summary - exceeds " + ActivityFeedDAO.MAX_LEN_ACTIVITY_SUMMARY + " chars: " + activitySummary); } else { feed.setActivitySummary(activitySummary); feed.setSiteNetwork(thisSite); feed.setAppTool(activityPost.getAppTool()); feed.setPostDate(activityPost.getPostDate()); feed.setPostId(activityPost.getId()); feed.setFeedDate(new Date()); // Insert activity feed insertFeedEntry(feed); // ignore returned feedId totalGenerated++; } } else { if (logger.isDebugEnabled()) { logger.debug("Empty template result for activityType '" + activityType + "' using format '" + formatFound + "' hence skip feed entry (activity post " + activityPost.getId() + ")"); } } } } } updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.PROCESSED); commitTransaction(); if (logger.isDebugEnabled()) { logger.debug("Processed: " + (recipients.size() - excludedConnections) + " connections for activity post " + activityPost.getId() + " (excluded " + excludedConnections + ")"); } } finally { endTransaction(); } } } catch (SQLException se) { logger.error(se); throw se; } finally { int postCnt = activityPosts == null ? 0 : activityPosts.size(); // TODO i18n info message StringBuilder sb = new StringBuilder(); sb.append("Generated ").append(totalGenerated).append(" activity feed entr") .append(totalGenerated == 1 ? "y" : "ies"); sb.append(" for ").append(postCnt).append(" activity post").append(postCnt != 1 ? "s" : "") .append(" (in ").append(System.currentTimeMillis() - startTime).append(" msecs)"); logger.info(sb.toString()); } } private Set<String> getRecipients(RepoCtx ctx, String siteId, String postUserId, String tenantDomain, Map<String, Set<String>> siteConnectedUsers, Map<Pair<String, String>, Set<String>> followerConnectedUsers, Map<Pair<String, String>, Boolean> canUserReadSite) throws Exception { // Recipients of this post Set<String> recipients = new HashSet<String>(); // Add site members to recipient list if ((null != siteId) && (siteId.length() > 0)) { // Get the members of this site - save hammering the repository by reusing cached site members Set<String> connectedUsers = siteConnectedUsers.get(siteId); if (connectedUsers == null) { try { // Repository callback to get site members connectedUsers = getSiteMembers(ctx, siteId, tenantDomain); connectedUsers.add(""); // add empty posting userid - to represent site feed ! } catch (Exception e) { throw new Exception("Failed to get site members: " + e); } // Cache them for future use (across activity posts handled) by this same invocation siteConnectedUsers.put(siteId, connectedUsers); } recipients.addAll(connectedUsers); } // Add followers to recipient list // MT Share Pair<String, String> userTenantKey = new Pair<String, String>(postUserId, tenantDomain); Set<String> followerUsers = followerConnectedUsers.get(userTenantKey); if (followerUsers == null) { try { followerUsers = getFollowers(postUserId, tenantDomain); } catch (Exception e) { throw new Exception("Failed to get followers: " + e); } // Cache them for future use (across activity posts handled) by this same invocation followerConnectedUsers.put(userTenantKey, followerUsers); } if ((null != siteId) && (siteId.length() > 0)) { for (String followerUser : followerUsers) { // MNT-13234 // avoid duplicate activities in activities feed boolean caseSensitive = ctx.isUserNamesAreCaseSensitive(); if ((caseSensitive && recipients.contains(followerUser)) || (!caseSensitive && (recipients.contains(followerUser) || recipients.contains(followerUser.toLowerCase())))) { continue; } Pair<String, String> userSiteKey = new Pair<String, String>(followerUser, siteId); Boolean canRead = canUserReadSite.get(userSiteKey); if (canRead == null) { // site read permission check (note: only check followers since implied for site members) canRead = canReadSite(ctx, siteId, followerUser, tenantDomain); canUserReadSite.put(userSiteKey, canRead); } if (canRead) { recipients.add(followerUser); } } } else { recipients.addAll(followerUsers); } // Add the originator to recipients recipients.add(postUserId); return recipients; } public abstract void startTransaction() throws SQLException; public abstract void commitTransaction() throws SQLException; public abstract void rollbackTransaction() throws SQLException; public abstract void endTransaction() throws SQLException; public abstract List<ActivityPostEntity> selectPosts(ActivityPostEntity selector) throws SQLException; public abstract List<FeedControlEntity> selectUserFeedControls(String userId) throws SQLException; public abstract long insertFeedEntry(ActivityFeedEntity feed) throws SQLException; public abstract int updatePostStatus(long id, ActivityPostEntity.STATUS status) throws SQLException; protected String callWebScript(String urlString, String ticket) throws MalformedURLException, URISyntaxException, IOException { URL url = new URL(urlString); if (logger.isDebugEnabled()) { logger.debug("Request URI: " + url.toURI()); } HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); if (ticket != null) { // add Base64 encoded authorization header // refer to: http://wiki.alfresco.com/wiki/Web_Scripts_Framework#HTTP_Basic_Authentication conn.addRequestProperty("Authorization", "Basic " + Base64.encodeBytes(ticket.getBytes())); } String result = null; InputStream is = null; BufferedReader br = null; try { is = conn.getInputStream(); br = new BufferedReader(new InputStreamReader(is)); String line = null; StringBuffer sb = new StringBuffer(); while (((line = br.readLine()) != null)) { sb.append(line); } result = sb.toString(); if (logger.isDebugEnabled()) { int responseCode = conn.getResponseCode(); logger.debug("Response code: " + responseCode); } } finally { if (br != null) { br.close(); } ; if (is != null) { is.close(); } ; } return result; } protected String getTenantName(String name, String tenantDomain) { // note: for MT impl, see override in LocalFeedTaskProcessor return name; } protected String getTenantDomain(String name) { // note: for MT impl, see override in LocalFeedTaskProcessor return TenantService.DEFAULT_DOMAIN; } protected Set<String> getSiteMembers(RepoCtx ctx, String siteId, String tenantDomain) throws Exception { // note: tenant domain ignored her - it should already be part of the siteId Set<String> members = new HashSet<String>(); if ((siteId != null) && (siteId.length() != 0)) { StringBuffer sbUrl = new StringBuffer(); sbUrl.append(ctx.getRepoEndPoint()).append(URL_SERVICE_SITES).append("/").append(siteId) .append(URL_MEMBERSHIPS); String jsonArrayResult = callWebScript(sbUrl.toString(), ctx.getTicket()); if ((jsonArrayResult != null) && (jsonArrayResult.length() != 0)) { JSONArray ja = new JSONArray(jsonArrayResult); for (int i = 0; i < ja.length(); i++) { JSONObject member = (JSONObject) ja.get(i); JSONObject person = (JSONObject) member.getJSONObject("person"); String userName = person.getString("userName"); if (!ctx.isUserNamesAreCaseSensitive()) { userName = userName.toLowerCase(); } members.add(userName); } } } return members; } protected abstract Set<String> getFollowers(String userId, String tenantDomain) throws Exception; protected abstract boolean canReadSite(final RepoCtx ctx, String siteIdIn, String connectedUser, final String tenantDomain) throws Exception; protected abstract boolean canRead(RepoCtx ctx, final String connectedUser, Map<String, Object> model) throws Exception; protected Map<String, List<String>> getActivityTypeTemplates(String repoEndPoint, String ticket, String subPath) throws Exception { StringBuffer sbUrl = new StringBuffer(); sbUrl.append(repoEndPoint).append(URL_SERVICE_TEMPLATES).append(subPath).append("*").append("?format=json"); String jsonArrayResult = null; try { jsonArrayResult = callWebScript(sbUrl.toString(), ticket); } catch (FileNotFoundException e) { return null; } List<String> allTemplateNames = new ArrayList<String>(10); if ((jsonArrayResult != null) && (jsonArrayResult.length() != 0)) { JSONArray ja = new JSONArray(jsonArrayResult); for (int i = 0; i < ja.length(); i++) { String name = ja.getString(i); allTemplateNames.add(name); } } return getActivityTemplates(allTemplateNames); } protected Map<String, List<String>> getActivityTemplates(List<String> allTemplateNames) { Map<String, List<String>> activityTemplates = new HashMap<String, List<String>>(10); for (String template : allTemplateNames) { if (!template.contains(" (Working Copy).")) { // assume template path = <path>/<base-activityType>.<format>.ftl // and base-activityType can contain "." String baseActivityType = template; int idx1 = baseActivityType.lastIndexOf("/"); if (idx1 != -1) { baseActivityType = baseActivityType.substring(idx1 + 1); } int idx2 = baseActivityType.lastIndexOf("."); if (idx2 != -1) { int idx3 = baseActivityType.substring(0, idx2).lastIndexOf("."); if (idx3 != -1) { baseActivityType = baseActivityType.substring(0, idx3); List<String> activityTypeTemplateList = activityTemplates.get(baseActivityType); if (activityTypeTemplateList == null) { activityTypeTemplateList = new ArrayList<String>(1); activityTemplates.put(baseActivityType, activityTypeTemplateList); } activityTypeTemplateList.add(template); } } } } return activityTemplates; } protected Configuration getFreemarkerConfiguration(RepoCtx ctx) { Configuration cfg = new Configuration(); cfg.setObjectWrapper(new DefaultObjectWrapper()); // custom template loader cfg.setTemplateLoader(new TemplateWebScriptLoader(ctx.getRepoEndPoint(), ctx.getTicket())); // TODO review i18n cfg.setLocalizedLookup(false); cfg.setIncompatibleImprovements(new Version(2, 3, 20)); cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); return cfg; } protected String processFreemarker(Map<String, Template> templateCache, String fmTemplate, Configuration cfg, Map<String, Object> model) throws IOException, TemplateException, Exception { // Save on lots of modification date checking by caching templates locally Template myTemplate = templateCache.get(fmTemplate); if (myTemplate == null) { myTemplate = cfg.getTemplate(fmTemplate); templateCache.put(fmTemplate, myTemplate); } StringWriter textWriter = new StringWriter(); myTemplate.process(model, textWriter); return textWriter.toString(); } protected List<FeedControlEntity> getFeedControls(String connectedUser) throws SQLException { //MNT-9104 If username contains uppercase letters the action of joining a site will not be displayed in "My activities" if (!userNamesAreCaseSensitive) { connectedUser = connectedUser.toLowerCase(); } return selectUserFeedControls(connectedUser); } protected boolean acceptActivity(ActivityPostEntity activityPost, List<FeedControlEntity> feedControls) { if (feedControls == null) { return true; } for (FeedControlEntity feedControl : feedControls) { if (ActivitiesDAO.KEY_ACTIVITY_NULL_VALUE.equals(feedControl.getSiteNetwork()) && (feedControl.getAppTool() != null)) { if (feedControl.getAppTool().equals(activityPost.getAppTool())) { // exclude this appTool (across sites) return false; } } else if (((feedControl.getAppTool() == null) || (feedControl.getAppTool().length() == 0)) && (feedControl.getSiteNetwork() != null)) { if (feedControl.getSiteNetwork().equals(activityPost.getSiteNetwork())) { // exclude this site (across appTools) return false; } } else if (((feedControl.getSiteNetwork() != null) && (feedControl.getSiteNetwork().length() > 0)) && ((feedControl.getAppTool() != null) && (feedControl.getAppTool().length() > 0))) { if ((feedControl.getSiteNetwork().equals(activityPost.getSiteNetwork())) && (feedControl.getAppTool().equals(activityPost.getAppTool()))) { // exclude this appTool for this site return false; } } } return true; } protected void addMissingFormats(String activityType, List<String> fmTemplates, List<String> templatesToAdd) { for (String templateToAdd : templatesToAdd) { int idx1 = templateToAdd.lastIndexOf("."); if (idx1 != -1) { int idx2 = templateToAdd.substring(0, idx1).lastIndexOf("."); if (idx2 != -1) { String templateFormat = templateToAdd.substring(idx2 + 1, idx1); boolean found = false; for (String fmTemplate : fmTemplates) { if (fmTemplate.contains("." + templateFormat + ".")) { found = true; } } if (!found) { if (logger.isDebugEnabled()) { logger.debug("Add template '" + templateToAdd + "' for type '" + activityType + "'"); } fmTemplates.add(templateToAdd); } } } } } protected String getTemplateSubPath(String activityType) { return (!activityType.startsWith("/") ? "/" : "") + activityType.replace(".", "/"); } protected String getBaseActivityType(String activityType) { String[] parts = activityType.split("\\."); return (parts.length != 0 ? parts[parts.length - 1] : ""); } protected class TemplateWebScriptLoader extends URLTemplateLoader { private String repoEndPoint; private String ticketId; public TemplateWebScriptLoader(String repoEndPoint, String ticketId) { this.repoEndPoint = repoEndPoint; this.ticketId = ticketId; } public URL getURL(String templatePath) { try { StringBuffer sb = new StringBuffer(); sb.append(this.repoEndPoint).append(URL_SERVICE_TEMPLATE).append("/").append(templatePath) .append("?format=text").append("&alf_ticket=").append(ticketId); if (logger.isDebugEnabled()) { logger.debug("getURL: " + sb.toString()); } return new URL(sb.toString()); } catch (Exception e) { throw new RuntimeException(e); } } } }