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

com.adobe.granite.rest.converter.siren.AbstractSirenConverter Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2014 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.granite.rest.converter.siren;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ArrayUtils;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.granite.rest.Constants;
import com.adobe.granite.rest.converter.ResourceConverter;
import com.adobe.granite.rest.converter.ResourceConverterContext;
import com.adobe.granite.rest.converter.ResourceConverterException;
import com.adobe.granite.rest.filter.Filter;
import com.adobe.granite.rest.utils.Resources;
import com.adobe.granite.rest.utils.URIUtils;
import com.adobe.reef.siren.Action;
import com.adobe.reef.siren.Entity;
import com.adobe.reef.siren.Link;
import com.adobe.reef.siren.builder.BuilderException;
import com.adobe.reef.siren.builder.EntityBuilder;
import com.adobe.reef.siren.builder.LinkBuilder;

/**
 * {@code AbtractConverter} is a base implementation of
 * {@link ResourceConverter}. ResourceConverter implementations are encouraged
 * to extend from this abstract base class.
 */
public abstract class AbstractSirenConverter implements ResourceConverter {

    /**
     * Allowed property prefixes for short version
     */
    // TODO: make this list configurable?
    protected static final String[] PREFIX_ALLOWED_SHORT = { "dc" };

    /**
     * Allowed property prefixes for long version
     */
    // TODO: make this list configurable?
    protected static final String[] PREFIX_ALLOWED = { "dc", "cq", "crs", "xmp" };

    /**
     * SIREN properties prefix
     */
    public static final String PREFIX_SRN = "srn:";

    protected Logger log = LoggerFactory.getLogger(getClass());

    protected Resource resource;

    private Collection children;

    /**
     * Relation attribute name "self".
     */
    public static final String REL_SELF = "self";

    /**
     * Relation attribute name "child".
     */
    public static final String REL_CHILD = "child";

    /**
     * Relation attribute name "content".
     */
    public static final String REL_CONTENT = "content";

    /**
     * Relation attribute name "next".
     */
    public static final String REL_NEXT = "next";

    /**
     * Relation attribute name "prev".
     */
    public static final String REL_PREV = "prev";

    /**
     * Relation attribute name "parent".
     */
    public static final String REL_PARENT = "parent";

    public AbstractSirenConverter(Resource resource) {
        this.resource = resource;
    }

    /**
     * Returns the child resources.
     * 
     * @return A collection of child resources
     */
    protected Collection listChildren() {
        if (children == null) {
            children = new LinkedList();
            for (Iterator it = resource.listChildren(); it.hasNext(); ) {
                Resource resource = it.next();
                children.add(resource);
            }
        }
        return children;
    }

    /**
     * Checks a {@code key} against a set of {@code allowedPrefixes}.
     * 
     * @param key Key to check
     * @param allowedPrefixes Array of allowed prefixes
     * @return {@code true} if key is allowed, {@code false} otherwise
     */
    protected boolean isAllowedPrefix(String key, String[] allowedPrefixes) {
        return isAllowedPrefix(key, null, allowedPrefixes);
    }

    /**
     * Checks a {@code key} against a set of {@code allowedPrefixes}.
     * 
     * @param key Key to check
     * @param context Converter context
     * @param allowedPrefixes Array of allowed prefixes
     * @return {@code true} if key is allowed, {@code false} otherwise
     */
    protected boolean isAllowedPrefix(String key, ResourceConverterContext context, String[] allowedPrefixes) {
        // Ignore props that start with an underscore as those are used internally
        if (key.charAt(0) == '_') {
            return false;
        }
        if (context != null && context.getShowProperties() != null && context.getShowProperties().length > 0) {
            boolean allowed = ArrayUtils.contains(context.getShowProperties(), key);
            if (allowed) {
                return allowed;
            }
        }
        String prefix = "";
        int pos = key.indexOf(':');
        // keys with no prefix are always allowed.
        if (pos < 0) {
            return true;
        }
        prefix = key.substring(0, pos);
        for (String p : allowedPrefixes) {
            if (p.equals(prefix)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns an array representation of the "class" property.
     * 
     * @return Siren class property values
     */
    protected abstract String[] getClazz();

    /**
     * Returns an object representation of properties. By default all properties
     * of the underlying resource matching the {@link #PREFIX_ALLOWED_SHORT} or
     * if {@code showAllProperties} is set to {@code true} the
     * {@link #PREFIX_ALLOWED} are returned.
     * 
     * @param context Converter context
     * @return Siren properties map
     */
    protected Map getProperties(ResourceConverterContext context) {
        return getProperties(context, false);
    }

    /**
     * Returns an object representation of properties. By default all properties
     * of the underlying resource matching the {@link #PREFIX_ALLOWED_SHORT} or
     * if {@code showAllProperties} is set to {@code true} the
     * {@link #PREFIX_ALLOWED} are returned.
     * 
     * @param context Converter context
     * @param isChild Indicator for child entity properties
     * @return Siren properties map
     */
    protected Map getProperties(ResourceConverterContext context, boolean isChild) {
        ValueMap valueMap = resource.adaptTo(ValueMap.class);
        Map props = getProperties(valueMap, context, isChild);
        
        // overwrite name
        props.put("name", resource.getName());
        return props;
    }

    /**
     * Returns a set of properties after applying the property prefix rules to
     * the specified {@code properties}.
     * 
     * @param properties Resource properties
     * @param context Converter context
     * @param isChild Indicator if resource is a child resource
     * @return
     */
    private Map getProperties(Map properties, ResourceConverterContext context, boolean isChild) {
        Map props = new HashMap();
        if (properties != null) {
            for (String key : properties.keySet()) {
                // show all allowed
                boolean isAllowed = isAllowedPrefix(key, context, PREFIX_ALLOWED);
                // show short allowed unless showAllProperties is set
                if (isChild) {
                    if (!context.isShowAllProperties()) {
                        isAllowed = isAllowedPrefix(key, context, PREFIX_ALLOWED_SHORT);
                    }
                }
                
                if (!isAllowed) {
                    continue;
                }
                
                Object value = properties.get(key);
                
                if(value instanceof Calendar) {
                    value = ISO8601.format((Calendar)value);
                }
                
                if (value instanceof ValueMap) {
                    value = getProperties((ValueMap)value, context, isChild);
                }
                
                props.put(key, value);
            }
        }
        return props;
    }

    /**
     * Returns a list of entities. By default returns an empty list.
     * 
     * @param context ResourceConverterContext
     * @param children Children resources to get entities from
     * @return List of Siren sub-entities
     * @throws BuilderException If an error occurs during the build of the Entity
     */
    protected List getEntities(ResourceConverterContext context, Iterator children) throws BuilderException {
        return new LinkedList();
    }

    /**
     * Returns a list of entities. By default returns an empty list.
     * 
     * @param context ResourceConverterContext
     * @return List of Siren sub-entities
     * @throws BuilderException If an error occurs during the build of the Entity
     */
    protected List getEntities(ResourceConverterContext context) throws BuilderException {
        return getEntities(context, listChildren().iterator());
    }

    /**
     * Returns an entity object.
     * 
     * @param clazz Class attribute.
     * @param title Title attribute. Optional.
     * @param actions Actions collection. Optional.
     * @param entities Entities collection. Optional.
     * @param links Links collection. Optional.
     * @param properties Properties collection. Optional.
     * @return Siren main entity
     * @throws BuilderException If an error occurs during the build of the Entity
     */
    protected Entity getEntity(String[] clazz, String title, List actions, List entities,
            List links, Map properties) throws BuilderException {
        Entity entity = new EntityBuilder()
                        .setClass(clazz)
                        .setTitle(title)
                        .setActions(actions)
                        .setEntities(entities)
                        .setLinks(links)
                        .setProperties(properties)
                        .build();
        
        return entity;
    }

    /**
     * Returns a list of links. By default this method returns a list containing
     * one link with rel attribute 'self' and the href pointing to itself.
     * 
     * @param context Converter context
     * @return List of Siren links
     * @throws BuilderException is an error occurs during building the link
     * @throws ResourceConverterException if general error occurs
     */
    protected List getLinks(ResourceConverterContext context) throws BuilderException, ResourceConverterException {
        Map pagingParameters = new HashMap();
        pagingParameters.putAll(context.getParameters());
        
        List links = new LinkedList();
        links.add(getLink(AbstractSirenConverter.REL_SELF, buildURL(context, resource.getPath(), Constants.EXT_JSON, pagingParameters), null));
        return links;
    }

    /**
     * Returns a link representation.
     * 
     * @param rel rel attribute
     * @param href href attribute
     * @param type type attribute
     * @return A Siren link
     * @throws BuilderException If an error occurs during the build of the Link
     */
    protected Link getLink(String[] rel, String href, String type) throws BuilderException {
        Link link = new LinkBuilder()
                        .setRel(rel)
                        .setHref(href)
                        .setType(type)
                        .build();
        return link;
    }

    /**
     * Returns a link representation.
     * 
     * @param rel rel attributes
     * @param href href attribute
     * @param type type attribute
     * @return A Siren link
     * @throws BuilderException If an error occurs during the build of the Link
     */
    protected Link getLink(String rel, String href, String type) throws BuilderException {
        return getLink(new String[]{rel}, href, type);
    }

    /**
     * Returns a list of actions. By default this method returns an empty list.
     * 
     * @param context Converter context
     * @return A list of Siren actions
     * @throws BuilderException If an error occurs during the build of the Actions
     */
    protected List getActions(ResourceConverterContext context) throws BuilderException, ResourceConverterException {
        return new LinkedList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Entity toEntity(ResourceConverterContext context) throws ResourceConverterException {
        try {
            ResourceConverterContext ctx = (ResourceConverterContext) context;
            Entity entity = new EntityBuilder()
                                .setClass(getClazz())
                                .setProperties(getProperties(ctx))
                                .setEntities(getEntities(ctx))
                                .setLinks(getLinks(ctx))
                                .setActions(getActions(ctx))
                                .build();
            return entity;
        } catch (BuilderException e) {
            throw new ResourceConverterException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Entity toSubEntity(ResourceConverterContext context) throws ResourceConverterException {
        try {
            List links = new LinkedList();
            links.add(getLink(AbstractSirenConverter.REL_SELF, buildURL(context, resource.getPath(), Constants.EXT_JSON), null));
            
            Entity entity = new EntityBuilder()
                                .setClass(getClazz())
                                .setRel(new String[]{AbstractSirenConverter.REL_CHILD})
                                .setProperties(getProperties(context,true))
                                .setLinks(links)
                                .build();
            return entity;
        } catch (BuilderException e) {
            throw new ResourceConverterException(e);
        }
    }

    /**
     * Builds an URL from the provided {@code resourcePath} and
     * {@code extension} parameters.
     * 
     * @param context Converter context. Cannot be null.
     * @param resourcePath Resource path to build a URL from. Cannot be null.
     * @param extension Resource extension with leading dot or {@code null}
     * @return A string representation of an URL
     * @throws ResourceConverterException if a general error occurs
     */
    protected String buildURL(ResourceConverterContext context, String resourcePath, String extension) throws ResourceConverterException {
        return buildURL(context, resourcePath, extension, null);
    }

    /**
     * Builds an URL pointing to the 'next' results for paging.
     * 
     * @param context
     *            Converter context
     * @return A string representation of an URL or {@code null} if there is
     *         nothing more to show
     * @throws ResourceConverterException
     *             if an error occurs
     */
    protected String getNextPageURL(ResourceConverterContext context) throws ResourceConverterException {
        int offset = context.getOffset();
        int limit = context.getLimit();
        Filter filter = context.getFilter();
        if (offset + limit >= Resources.getSize(listChildren().iterator(), filter)) {
            return null;
        }
        offset += limit;
        return buildPagingURL(context, offset);
    }

    /**
     * Builds an URL pointing to the 'previous' results for paging.
     * 
     * @param context Converter context
     * @return A string representation of an URL
     * @throws ResourceConverterException if a general error occurs
     */
    protected String getPrevPageURL(ResourceConverterContext context) throws ResourceConverterException {
        int offset = context.getOffset();
        if (offset == 0) {
            return null;
        }
        int limit = context.getLimit();
        offset -= limit;
        if (offset < 0) {
            offset = 0;
        }
        return buildPagingURL(context, offset);
    }

    private String buildPagingURL(ResourceConverterContext context, int offset) throws ResourceConverterException {
        Map pagingParameters = new LinkedHashMap();
        pagingParameters.putAll(context.getParameters());
        pagingParameters.put(Constants.PARAM_OFFSET, new String[]{ Integer.toString(offset) } );
        pagingParameters.put(Constants.PARAM_LIMIT, new String[]{ Integer.toString(context.getLimit())});
        return buildURL(context, resource.getPath(), Constants.EXT_JSON, pagingParameters);
    }

    private String buildURL(ResourceConverterContext context, String resourcePath, String extension, Map additionalParameters) 
            throws ResourceConverterException {

        String authority = context.getServerName();
        // we won't expose default ports
        if (context.getServerPort() != 80 
                && context.getServerPort() != 443) {
            authority += ":" + context.getServerPort();
        }

        String path = resourcePath;
        if (!context.isAbsolutURI()) {
            path = URIUtils.relativize(context.getRequestPathInfo(), resourcePath);
        }

        if (extension != null) {
            path += extension;
        }

        String query = null;
        if (additionalParameters != null) {
            boolean isFirst = true;
            for (String parameter : additionalParameters.keySet()) {
                for (String value : additionalParameters.get(parameter)) {
                    if (!isFirst) {
                        query += "&";
                    } else {
                        query = "";
                        isFirst = false;
                    }
                    query += parameter;
                    query += "=";
                    query += value;
                }
            }
        }

        try {
            if (!context.isAbsolutURI()) {
                return new URI( null, 
                                null,
                                path, 
                                query, 
                                null
                               ).toASCIIString();
            } else {
                return new URI( context.getScheme(), 
                                authority,
                                path, 
                                query, 
                                null
                               ).toASCIIString();
            }
        } catch (URISyntaxException e) {
            throw new ResourceConverterException(e.getMessage(), e);
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy