All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.day.cq.commons.servlets.AbstractListServlet Maven / Gradle / Ivy

/*
 * Copyright 1997-2010 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */
package com.day.cq.commons.servlets;

import java.io.IOException;
import java.io.StringWriter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import javax.jcr.Session;
import javax.servlet.ServletException;

import org.apache.commons.collections.Predicate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.json.io.JSONWriter;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.commons.ListInfoProvider;
import com.day.cq.commons.TidyJSONWriter;

/**
 * The AbstractListServlet provides base functionality such as
 * sorting and paging for servlets that feed Ext grids (like in the SiteAdmin)
 * with JSON.
 * Normally, the list of children of the addressed resource are returned.
 * Alternatively, the paging index of an item can be requested.
 */
@Component(metatype = false, componentAbstract = true)
public class AbstractListServlet extends AbstractPredicateServlet {

    /**
     * Default logger
     */
    private static final Logger log = LoggerFactory.getLogger(AbstractListServlet.class);

    /**
     * Collator instance
     */
    private Collator collator = Collator.getInstance();

    protected static final long serialVersionUID = 2138470595710406273L;

    /**
     * Parameter to use for tidy JSON. If present, indentation and line breaks are
     * added for better legibility.
     */
    public static final String TIDY = "tidy";

    /**
     * Parameter to specify the start index with when using paging.
     * Typically used in conjunction with {@link #PAGE_LIMIT}.
     * For the items of the page n, use a start index of
     * limit + (n - 1).
     */
    public static final String PAGE_START = "start";

    /**
     * Parameter to specify the limit of items per page when using paging.
     * Typically used in conjunction with {@link #PAGE_START}.
     */
    public static final String PAGE_LIMIT = "limit";

    /**
     * Parameter to specify which property use for sorting. Defaults to "index".
     */
    public static final String SORT_KEY = "sort";

    /**
     * Parameter to specify the direction to use for sorting. Defaults to
     * {@link #SORT_ASCENDING}.
     */
    public static final String SORT_DIR = "dir";

    /**
     * Value to use for {@link #SORT_DIR} to use ascending order (default).
     */
    public static final String SORT_ASCENDING = "ASC";

    /**
     * Value to use for {@link #SORT_DIR} to use descending order.
     */
    public static final String SORT_DESCENDING = "DESC";

    /**
     * Parameter to use in conjunction with {@link #PAGE_INDEX} to determine the
     * paging index of an item. If both parameters are present, the paging
     * index of the item with the specified path will be returned instead of the
     * list of children.
     */
    public static final String PATH = "path";

    /**
     * Parameter to use in conjunction with {@link #PATH} to determine the
     * paging index of an item. If this parameter is present, the paging
     * index of the item with the path specified in {@link #PATH} will be returned
     * instead of the list of children.
     */
    public static final String PAGE_INDEX = "index";

    /**
     * Parameter to use to specify the name(s) of custom properties that should be
     * returned for each item in the list. If the properties exist on the item's
     * resource, their values will be returned as additional JSON properties.
     */
    public static final String PROP = "prop";

//    /**
//     * Parameter to use to specify the resource type to use to render a single
//     * list item when doing HTML rendering.
//     */
//    public static final String ITEM_RESOURCE_TYPE = "itemResourceType";
//
    protected static final String DEFAULT_TIDY = "true";

    protected static final String DEFAULT_SORT_KEY = "index";

    protected static final String DEFAULT_SORT_DIR = SORT_ASCENDING;

    protected static final String CONTENT_TYPE = "application/json";

//    protected static final String HTML_CONTENT_TYPE = "text/html";
//
    protected static final String ENCODING = "utf-8";

    // ----< services >---------------------------------------------------------

    @Reference(
            policy = ReferencePolicy.STATIC
    )
    @SuppressWarnings( { "UnusedDeclaration" })
    protected SlingRepository repository;

    @Reference(
            cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
            policy = ReferencePolicy.DYNAMIC
    )
    @SuppressWarnings( { "UnusedDeclaration" })
    protected ListInfoProvider listInfoProvider;

    // ----< members >----------------------------------------------------------

    /**
     * @deprecated The admin session is no longer available and will be always {@code null}. Extending components requiring specific
     * permissions should get their own session using the {@link SlingRepository#loginService(String, String)}.
     */
    @Deprecated
    protected Session admin = null;

    private final List delayedProviders = new ArrayList();

    private final List providers = new ArrayList();

    private List cachedProviders = Collections.emptyList();

    private ComponentContext componentContext;

    // ----< servlet methods >--------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doGet(SlingHttpServletRequest request,
                         SlingHttpServletResponse response, Predicate predicate)
            throws ServletException, IOException {

        try {
            // get list items
            List items = getItems(request, predicate);
            int total = items.size();

//            boolean isHTML = "html".equals(request.getRequestPathInfo().getExtension());
//            if (!isHTML) {
                if (request.getParameter(PAGE_INDEX) != null) {
                    // return page index of item with given path
                    String path = request.getParameter(PATH);
                    writePagingIndex(request, response, items, path);
                    return;
                }
//            }

            items = processItems(request, items, total);

//            if (isHTML) {
//                writeHtml(request, response, items);
//            } else {
                // get custom properties
                String[] customProps = request.getParameterValues(PROP);

                write(request, response, items, customProps, total);
//            }

        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    // ----< internal >---------------------------------------------------------

    /**
     * Returns the list items based on the specified request and predicate.
     * The list items are typically generated from the children of the
     * Resource provided by the request. The optional predicate
     * can be used for evaluation.
     * @param request The request
     * @param predicate The predicate (optional)
     * @return The list items
     * @throws Exception if unable to retrieve items
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected List getItems(SlingHttpServletRequest request,
                             Predicate predicate) throws Exception {
        // implement in subclass
        return null;
    }

    /**
     * Processes the specified list items based on the request parameters.
     * By default, sorting and paging is applied.
     * @see #applySorting(SlingHttpServletRequest, List)
     * @see #applyPaging(SlingHttpServletRequest, List, int)
     * @param request The request
     * @param items The list items
     * @param total The total number of list items
     * @return The processed list items
     */
    protected List processItems(SlingHttpServletRequest request,
                                          List items, int total) {
        return applyPaging(request, applySorting(request, items), total);
    }

    /**
     * Applies sorting to the list items if specified in the request.
     * @param request The request
     * @param items The list items
     * @return The sorted list items
     */
    protected List applySorting(SlingHttpServletRequest request,
                                          List items) {
        String sortKey = request.getParameter(SORT_KEY) != null ?
                request.getParameter(SORT_KEY) : DEFAULT_SORT_KEY;
        String sortDir = request.getParameter(SORT_DIR) != null ?
                request.getParameter(SORT_DIR) : DEFAULT_SORT_DIR;

        /* set collator strength */
        collator.setStrength(Collator.PRIMARY);

        // apply sorting
        if (!(DEFAULT_SORT_KEY.equals(sortKey) && DEFAULT_SORT_DIR.equals(sortDir))) {
            Collections.sort(items, new ListItemComparator(sortKey));
            if (SORT_DESCENDING.equals(sortDir)) {
                Collections.reverse(items);
            }
        }
        return items;
    }

    /**
     * Applies paging to the list items if specified in the request.
     * @param request The request
     * @param items The list items
     * @param total The total number of list items
     * @return The truncated list items
     */
    protected List applyPaging(SlingHttpServletRequest request,
                                          List items, int total) {
        int start = 0;
        int end = Integer.MAX_VALUE;
        if (request.getParameter(PAGE_START) != null) {
            try {
                start = Integer.parseInt(request.getParameter(PAGE_START));
            } catch (Exception ignored) {
            }
        }
        if (request.getParameter(PAGE_LIMIT) != null) {
            try {
                end = Integer.parseInt(request.getParameter(PAGE_LIMIT)) + start;
            } catch (Exception ignored) {
            }
        }

        // truncate list
        if (start > 0 || total > end - start) {
            if (start > total - 1) {
                start = total;
            }
            if (end > total - 1) {
                end = total;
            }
            items = items.subList(start, end);
        }
        return items;
    }

    /**
     * Returns the paging index of the item with the specified path or
     * -1 if item not part of the specified items.
     * @param request The request
     * @param items The list items
     * @param path The path of the item
     * @return The paging index
     */
    protected long getPagingIndex(SlingHttpServletRequest request, List items, String path) {
        if (path == null) {
            return -1;
        }
        int limit = Integer.MAX_VALUE;
        if (request.getParameter(PAGE_LIMIT) != null) {
            try {
                limit = Integer.parseInt(request.getParameter(PAGE_LIMIT));
            } catch (Exception ignored) {
            }
        }
        Iterator iterator = items.iterator();
        long index = 0;
        int counter = 0;
        boolean found = false;
        while (iterator.hasNext()) {
            if (path.equals(((ListItem)iterator.next()).getResource().getPath())) {
                found = true;
                break;
            }
            if (++counter == limit) {
                index++;
                counter = 0;
            }
        }
        return found ? index : -1;
    }

    /**
     * Writes the list to a JSON writer.
     * @param request The request
     * @param response The response
     * @param listItems The list items
     * @param customProps The names of the custom properties to include
     * @param total The total number of list items
     * @throws Exception if unable to write the list
     */
    protected void write(SlingHttpServletRequest request,
                              SlingHttpServletResponse response,
                              List listItems,
                              String[] customProps, int total)
            throws Exception {
        response.setContentType(CONTENT_TYPE);
        response.setCharacterEncoding(ENCODING);

        // Final result
        JSONObject json = new JSONObject();

        // Get list of custom providers
        List listInfoProviders = cachedProviders;

        // Items list
        JSONArray listArray = new JSONArray();
        for (ListItem item : listItems) {
            // Base list item information
            StringWriter out = new StringWriter();
            JSONWriter writer = new TidyJSONWriter(out);
            writer.setTidy(DEFAULT_TIDY.equals(request.getParameter(TIDY)));
            item.write(writer, customProps);

            // Reparse object
            JSONObject info = new JSONObject(out.toString());

            // Additional item information from configured list info providers
            Resource resource = item.getResource();
            for (ListInfoProvider p : listInfoProviders) {
                long t0 = System.currentTimeMillis();
                p.updateListItemInfo(request, info, resource);
                long t1 = System.currentTimeMillis();
                log.debug("{}.updateListItemInfo() in {}ms", p.getClass().getName(), t1 - t0);
            }

            // Append object to array
            listArray.put(info);
        }
        json.put("pages", listArray);

        // Global list information
        json.put("results", total);

        // Add global information from list info providers
        Resource resource = request.getResource();
        for (ListInfoProvider p : listInfoProviders) {
            long t0 = System.currentTimeMillis();
            p.updateListGlobalInfo(request, json, resource);
            long t1 = System.currentTimeMillis();
            log.debug("{}.updateListGlobalInfo() in {}ms", p.getClass().getName(), t1 - t0);
        }

        // Write JSON response
        json.write(response.getWriter());
    }

//    /**
//     * Writes the list as HTML.
//     * @param request The request
//     * @param response The response
//     * @param listItems The list items
//     * @throws Exception if unable to write the list
//     */
//    protected void writeHtml(SlingHttpServletRequest request,
//                         SlingHttpServletResponse response,
//                         List listItems)
//            throws Exception {
//        response.setContentType(HTML_CONTENT_TYPE);
//        response.setCharacterEncoding(ENCODING);
//
//        // Get item resource type
//        String itemResourceType = request.getParameter(ITEM_RESOURCE_TYPE);
//        if (itemResourceType == null || request.getResourceResolver().getResource(itemResourceType) == null) {
//            log.error("No resource type to render list items");
//            return;
//        }
//
//        // Items list
//        RequestDispatcherOptions requestDispatcherOptions = new RequestDispatcherOptions(null);
//        requestDispatcherOptions.setForceResourceType(itemResourceType);
//        requestDispatcherOptions.setReplaceSelectors("");
//
//        for (ListItem item : listItems) {
//            RequestDispatcher dispatcher = request.getRequestDispatcher(item.getResource().getPath() + ".html", requestDispatcherOptions);
//            dispatcher.include(request, response);
//        }
//    }
//
    /**
     * Writes a key and its value to the specified JSON writer.
     * @param out The JSON
     * @param key The name of the key
     * @param value The value of the key
     * @throws JSONException if unable to write the key
     */
    protected void writeKey(JSONWriter out, String key, Object value)
            throws JSONException {
        out.key(key).value(value);
    }

    /**
     * Writes a key and its value to the specified JSON writer. If the
     * value is null, the key is omitted.
     * @param out The JSON
     * @param key The name of the key
     * @param value The value of the key
     * @throws JSONException if unable to write the key
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void writeOptionalKey(JSONWriter out, String key, Object value)
            throws JSONException {
        if (value != null) {
            writeKey(out, key, value);
        }
    }

    /**
     * Writes a key and its date value to the specified JSON writer. If the
     * value is null, the key is omitted.
     * @param out The JSON
     * @param key The name of the key
     * @param value The date value of the key
     * @throws JSONException if unable to write the key
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void writeOptionalDateKey(JSONWriter out, String key, Calendar value)
            throws JSONException {
        if (value != null) {
            out.key(key).value(value.getTimeInMillis());
        }
    }

    /**
     * Writes the specified properties of the resource to the JSON writer.
     * If the value is null, the key is omitted.
     * @param out The JSON
     * @param resource The resource
     * @param customProps The properties
     * @throws JSONException if unable to write the properties
     */
    @SuppressWarnings({"UnusedDeclaration"})
    protected void writeCustomProperties(JSONWriter out, Resource resource,
                                         String[] customProps) throws JSONException {
        if (customProps != null) {
            ValueMap props = resource.adaptTo(ValueMap.class);
            if (props != null) {
                for (String name : customProps) {
                    writeOptionalKey(out, name, props.get(name, null));
                }
            }
        }
    }

    /**
     * Writes the paging index of the item with the specified path to a JSON writer.
     * @param request The request
     * @param response The response
     * @param listItems The list items
     * @param path The path of the item
     * @throws Exception if unable to write the index
     */
    protected void writePagingIndex(SlingHttpServletRequest request,
                              SlingHttpServletResponse response,
                              List listItems, String path) throws Exception {
        response.setContentType(CONTENT_TYPE);
        response.setCharacterEncoding(ENCODING);
        JSONWriter writer = new JSONWriter(response.getWriter());
        writer.setTidy(DEFAULT_TIDY.equals(request.getParameter(TIDY)));
        writer.object();
        writer.key(PAGE_INDEX).value(getPagingIndex(request, applySorting(request, listItems), path));
        writer.endObject();
    }

    // ----< SCR Integration >--------------------------------------------------

    @SuppressWarnings({"UnusedDeclaration"})
    protected void activate(ComponentContext context) throws Exception {
        synchronized (delayedProviders) {
            componentContext = context;
            for (ServiceReference ref : delayedProviders) {
                registerProvider(ref);
            }
            delayedProviders.clear();
        }
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected void deactivate(ComponentContext context) {
        componentContext = null;
    }

    /**
     * The ListItem interface defines a sortable item of the list.
     * Sortable fields must be public for comparison.
     * @see ListItemComparator
     */
    @SuppressWarnings({"UnusedDeclaration"})
    public interface ListItem {

        public static final String INDEX = "index";
        public static final String PATH = "path";
        public static final String LABEL = "label";
        public static final String TYPE = "type";
        public static final String TITLE = "title";
        public static final String DESCRIPTION = "description";
        public static final String LAST_MODIFIED = "lastModified";
        public static final String LAST_MODIFIED_BY = "lastModifiedBy";
        public static final String LOCKED_BY = "lockedBy";
        public static final String MONTHLY_HITS = "monthlyHits";

        public static final String REPLICATION = "replication";
        public static final String REPLICATION_NUM_QUEUED = "numQueued";
        public static final String REPLICATION_ACTION = "action";
        public static final String REPLICATION_PUBLISHED = "published";
        public static final String REPLICATION_PUBLISHED_BY = "publishedBy";

        public static final String IN_WORKFLOW = "inWorkflow";
        public static final String WORKFLOWS = "workflows";
        public static final String WORKFLOW_MODEL = "model";
        public static final String WORKFLOW_STARTED = "started";
        public static final String WORKFLOW_STARTED_BY = "startedBy";
        public static final String WORKFLOW_SUSPENDED = "suspended";
        public static final String WORKFLOW_WORK_ITEMS = "workItems";
        public static final String WORKFLOW_WORK_ITEM_TITLE = "item";
        public static final String WORKFLOW_WORK_ITEM_ASSIGNEE = "assignee";

        public static final String SCHEDULED_TASKS = "scheduledTasks";
        public static final String SCHEDULED_TASK_VERSION = "version";
        public static final String SCHEDULED_TASK_SCHEDULED = "scheduled";
        public static final String SCHEDULED_TASK_SCHEDULED_BY = "scheduledBy";
        public static final String SCHEDULED_TASK_TYPE = "type";

        public static final String SCHEDULED_ACTIVATION_WORKFLOW_ID =
                "/etc/workflow/models/scheduled_activation/jcr:content/model";

        public static final String SCHEDULED_DEACTIVATION_WORKFLOW_ID =
                "/etc/workflow/models/scheduled_deactivation/jcr:content/model";

        /**
         * Writes the list item to the specified JSON writer.
         * @param out The writer
         * @param customProps The names of the custom properties to include
         * @throws Exception unable to write list item
         */
        public void write(JSONWriter out, String[] customProps) throws Exception;

        /**
         * Get item resource
         * @return Item resource
         */
        public Resource getResource();
    }

    /**
     * The ListItemComparator compares public fields of
     * {@link ListItem}s.
     */
    public class ListItemComparator implements Comparator {

        private String compareField;

        /**
         * Creates a new ListItemComparator instance.
         * @param compareField The public field to compare
         */
        public ListItemComparator(String compareField) {
            this.compareField = compareField;
        }

        /**
         * {@inheritDoc}
         */
        public int compare(ListItem o1, ListItem o2) {
            Object v1, v2;
            try {
                v1 = o1.getClass().getField(compareField).get(o1);
                v2 = o2.getClass().getField(compareField).get(o2);
                if (v1 instanceof String && v2 instanceof String) {
                    return (collator != null) ? collator.compare((String)v1, (String)v2) : ((String)v1).compareTo((String)v2);
                } else if (v1 instanceof Integer && v2 instanceof Integer) {
                    int int1 = (Integer)v1;
                    int int2 = (Integer)v2;
                    return (int1 > int2 ? 1 : int1 != int2 ? -1 : 0);
                } else if (v1 instanceof Long && v2 instanceof Long) {
                    long long1 = (Long)v1;
                    long long2 = (Long)v2;
                    return (long1 > long2 ? 1 : long1 != long2 ? -1 : 0);
                }
            } catch (Exception ignored) {
            }
            return 0;
        }

    }

    protected void bindListInfoProvider(ServiceReference ref) {
        synchronized (delayedProviders) {
            if (componentContext == null) {
                delayedProviders.add(ref);
            } else {
                registerProvider(ref);
            }
        }
    }

    protected void unbindListInfoProvider(ServiceReference ref) {
        synchronized (delayedProviders) {
            this.delayedProviders.remove(ref);
            this.providers.remove(ref);
            updateCachedProviders();
        }
    }

    protected void registerProvider(ServiceReference ref) {
        providers.add(ref);
        updateCachedProviders();
    }

    protected void updateCachedProviders() {
        if(componentContext == null) {
            return;
        }
        List pvs = new ArrayList();
        for (ServiceReference current : providers) {
            ListInfoProvider provider = (ListInfoProvider) componentContext.locateService("listInfoProvider", current);
            if (provider != null) {
                pvs.add(provider);
            }
        }
        cachedProviders = pvs;
    }

    protected Collator getCollator() {
        return collator;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy