org.alfresco.repo.activities.feed.FeedTaskProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.activities.feed.FeedTaskProcessor.java

Source

/*
 * #%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);
            }
        }
    }
}