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.Collections;
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.commons.lang.StringUtils;
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.haf.apimodel.impl.ApiModelWrapper;
import com.adobe.granite.haf.apimodel.internal.FilterableChildren;
import com.adobe.granite.haf.api.OrderByDetails;
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;
    }

    protected Collection filterChildren(Map filters, int offset, int limit) {
        if (children == null) {
            children = new LinkedList();
            if (filters != null && !filters.isEmpty() && resource instanceof FilterableChildren) {
                for (Iterator it =
                    ((FilterableChildren) resource).filterChildren(filters, Integer.toString(offset), limit,
                        Collections.emptyList()); it.hasNext();) {
                    Resource resource = it.next();
                    children.add(resource);
                }
            }
        }
        return children;

    }

    /**
     * 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) {
        if (!(resource instanceof ApiModelWrapper)) {
            ValueMap valueMap = resource.adaptTo(ValueMap.class);
            Map props = getProperties(valueMap, context, isChild);
            // overwrite name
            props.put("name", resource.getName());
            return props;
        } else {
            return new HashMap<>();
        }
    }

    /**
     * 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 {
        if (!context.getModelFilters().isEmpty()) {
            return getEntities(context,
                filterChildren(context.getModelFilters(), context.getOffset(), context.getLimit()).iterator());
        }
        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 {
        return getLinks(context, false);
    }

    protected List getLinks(ResourceConverterContext context, boolean isChild) throws BuilderException,
        ResourceConverterException {
        Map pagingParameters = new HashMap();
        if (context.getParameters() != null) {
            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);
        }
    }

    @Override
    public Entity toSubEntity(ResourceConverterContext context) throws ResourceConverterException {
        try {
            Entity entity =
                new EntityBuilder().setClass(getClazz()).setRel(new String[]{AbstractSirenConverter.REL_CHILD})
                    .setProperties(getProperties(context, true)).setLinks(getLinks(context, true)).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 (!context.getModelFilters().isEmpty()) {
            if (offset + limit >= Resources.getSize(filterChildren(context.getModelFilters(), offset, limit)
                .iterator(), filter)) {
                return null;
            }
        }
        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 =
            (StringUtils.isEmpty(context.getContextPath()) ? "" : StringUtils
                .removeEnd(context.getContextPath(), "/")) + 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 - 2024 Weber Informatics LLC | Privacy Policy