play.modules.resteasy.crud.RESTResource.java Source code

Java tutorial

Introduction

Here is the source code for play.modules.resteasy.crud.RESTResource.java

Source

/*
This file is part of resteasy-crud-play-module.
    
Copyright Lunatech Research 2010
    
resteasy-crud-play-module 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.
    
resteasy-crud-play-module 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 General Lesser Public License
along with resteasy-crud-play-module.  If not, see <http://www.gnu.org/licenses/>.
*/
package play.modules.resteasy.crud;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;
import org.hibernate.validator.NotEmpty;
import org.hibernate.validator.NotNull;

import play.Logger;
import play.db.Model;
import play.db.Model.Factory;
import play.db.Model.Manager;
import play.db.Model.Property;

/**
 * Your CRUD resource must subclass this to gain automagic resources.
 * 
 * @author Stphane pardaud <stef@epardaud.fr>
 */
public abstract class RESTResource {

    /**
     * Queries the permission system for a permission
     * @param target the object we want a permission for, can be a Class if this is a general permission
     * @param name the name of the permission
     * @return true if the current user has this permission, false otherwise
     */
    protected boolean hasPermission(Object target, String name) {
        return CRUDSecure.hasPermission(target, name);
    }

    /**
     * Throws a FORBIDDEN exception if the user doesn't have the given permission
     * @param target the object we want a permission for, can be a Class if this is a general permission
     * @param name the name of the permission
     */
    protected void checkPermission(Object target, String name) {
        if (!hasPermission(target, name))
            throw new WebApplicationException(HttpURLConnection.HTTP_FORBIDDEN);
    }

    /**
     * Throws a NOT_FOUND if the given parameter is null
     * @param <T> the type of parameter
     * @param o the parameter to check
     * @return the parameter
     */
    protected <T> T checkNotFound(T o) {
        if (o == null)
            throw new WebApplicationException(HttpURLConnection.HTTP_NOT_FOUND);
        return o;
    }

    /**
     * Throws a NOT_FOUND with the given message if the given parameter is null
     * @param <T> the type of parameter
     * @param o the parameter to check
     * @param msg the message format
     * @param params the message parameters
     * @return the parameter
     */
    protected <T> T checkNotFound(T o, String msg, Object... params) {
        if (o == null)
            throw new WebApplicationException(
                    Response.status(HttpURLConnection.HTTP_NOT_FOUND).entity(String.format(msg, params)).build());
        return o;
    }

    /**
     * Throws BAD_REQUEST if the given parameters are not null or are not empty collections
     * @param objects the objects to check
     */
    protected void checkEmpty(Object... objects) {
        for (Object o : objects) {
            if (o == null)
                continue;
            if (o instanceof Collection && ((Collection<?>) o).isEmpty())
                continue;
            throw new WebApplicationException(HttpURLConnection.HTTP_BAD_REQUEST);
        }
    }

    /**
     * Checks that we have the insert permission for the o object, and that oDB is not null and we have the update permission on it.
     * @param <T> the parameter type
     * @param o the new object we want to get values from
     * @param oDB the old object we want to update
     * @return the old object
     */
    protected <T> T checkForUpdate(T o, T oDB) {
        checkPermission(o, "insert");
        checkNotFound(oDB);
        checkPermission(oDB, "update");
        return oDB;
    }

    /**
     * Override if you have special checks for autocomplete queries
     * @param values the autocomplete query values
     */
    protected void checkAutoCompleteQuery(String... values) {
    }

    /**
     * Returns a NOT_FOUND response
     */
    protected Response notFound() {
        return status(HttpURLConnection.HTTP_NOT_FOUND);
    }

    /**
     * Returns a FORBIDDEN response
     */
    protected Response forbidden() {
        return status(HttpURLConnection.HTTP_FORBIDDEN);
    }

    /**
     * Returns a BAD_REQUEST response
     */
    protected Response badRequest() {
        return status(HttpURLConnection.HTTP_BAD_REQUEST);
    }

    /**
     * Returns a BAD_REQUEST response with the specified message
     * @param message the message format
     * @param args the message parameters
     */
    protected Response badRequest(String message, Object... args) {
        return status(HttpURLConnection.HTTP_BAD_REQUEST, message, args);
    }

    /**
     * Returns a NO_CONTENT response
     */
    protected Response noContent() {
        return status(HttpURLConnection.HTTP_NO_CONTENT);
    }

    /**
     * Returns a CREATED response
     */
    protected Response created() {
        return status(HttpURLConnection.HTTP_CREATED);
    }

    /**
     * Returns a CREATED response with a Location header
     */
    protected Response created(URI location) {
        return status(HttpURLConnection.HTTP_CREATED, location);
    }

    /**
     * Returns an INTERNAL_ERROR response
     */
    protected Response internalError() {
        return status(HttpURLConnection.HTTP_INTERNAL_ERROR);
    }

    /**
     * Returns an OK response
     */
    protected Response ok() {
        return status(HttpURLConnection.HTTP_OK);
    }

    /**
     * Returns an OK response with the given entity
     * @param entity the entity to send
     */
    protected Response ok(Object entity) {
        return status(HttpURLConnection.HTTP_OK, entity);
    }

    /**
     * Returns an INTERNAL_ERROR response with the specified message
     * @param message the message format
     * @param args the message parameters
     */
    protected Response internalError(String msg, Object... args) {
        return status(HttpURLConnection.HTTP_INTERNAL_ERROR, msg, args);
    }

    /**
     * Returns an INTERNAL_ERROR response with the specified message and logs the given error
     * @param t the error to log
     * @param message the message format
     * @param args the message parameters
     */
    protected Response internalError(Throwable t, String msg, Object... args) {
        Logger.error(t, msg, args);
        return status(HttpURLConnection.HTTP_INTERNAL_ERROR, msg, args);
    }

    /**
     * Returns a response with the specified status code
     * @param code the status code
     */
    protected Response status(int code) {
        Logger.info("Returning code %s", code);
        return Response.status(code).build();
    }

    /**
     * Returns a response with the specified status code and message
     * @param code the status code
     * @param message the message format
     * @param args the message parameters
     */
    protected Response status(int code, String message, Object... args) {
        String entity = String.format(message, args) + "\n";
        return status(code, entity);
    }

    /**
     * Returns a response with the specified status code and entity
     * @param code the status code
     * @param entity the response entity
     */
    protected Response status(int code, Object entity) {
        Logger.info("Returning code %s: %s", code, entity);
        return Response.status(code).entity(entity).build();
    }

    /**
     * Returns a response with the specified status code and Location header
     * @param code the status code
     * @param location the Location header
     */
    protected Response status(int code, URI location) {
        Logger.info("Returning code %s: %s", code, location);
        return Response.status(code).location(location).build();
    }

    /**
     * Throws a WebApplicationException with the given response
     * @param r the response to send to the client
     */
    protected void respond(Response r) {
        throw toThrowable(r);
    }

    /**
     * Makes a WebApplicationException with the given response
     * @param r the response to wrap in a WebApplicationException
     * @return the WebApplicationException
     */
    protected WebApplicationException toThrowable(Response r) {
        return new WebApplicationException(r);
    }

    /**
     * Makes an OK response with the given list of autocomplete results as entity
     * @param list the list of autocomplete results
     */
    protected Response autoComplete(List<String> list) {
        return ok(new JAXBList(list));
    }

    /**
     * Throws an InvalidStateException for the given validation error
     * @param entity the entity with an invalid value
     * @param message the error message
     * @param field the invalid field
     * @param value the invalid value
     */
    protected void throwConstraintValidation(Object entity, String message, String field, Object value) {
        throw new InvalidStateException(
                new InvalidValue[] { new InvalidValue(message, entity.getClass(), field, value, entity) });
    }

    //
    // CRUD

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for getting a list of entities.
     * @param model the model type
     * @param q the query for the list of entities
     * @return a response with the list of entities
     */
    public <T extends Model> Response list(Class<T> model, DataTableQuery q) {
        logQuery(q);
        checkPermission(model, "select");
        PagedQuery<T> carriers = findPaged(model);
        return makeQueryResponse(q, carriers, model, getSortableColumns(model), q.uriInfo);
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for getting a single entity.
     * @param model the model type
     * @param id the entity id
     * @return a response with entity
     */
    public <T extends Model> Response get(Class<T> model, Object id) {
        Factory factory = Manager.factoryFor(model);
        @SuppressWarnings("unchecked")
        T entity = (T) factory.findById(id);
        checkNotFound(entity, "Entity of type %s with id of %s could not be found", model.getName(), id);
        checkPermission(entity, "select");
        return ok(entity);
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for deleting an entity
     * @param model the model type
     * @param id the entity id
     * @return a response with no content
     */
    public <T extends Model> Response delete(Class<T> model, Object id) {
        Factory factory = Manager.factoryFor(model);
        @SuppressWarnings("unchecked")
        T entity = (T) factory.findById(id);
        checkNotFound(entity, "Entity of type %s with id of %s could not be found", model.getName(), id);
        checkPermission(entity, "delete");
        entity._delete();
        return noContent();
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for adding an entity.
     * @param model the model type
     * @param elem the new entity
     * @return a response with no content
     */
    public <T extends Model> Response add(Class<T> model, final T elem, UriInfo uriInfo) {
        checkPermission(elem, "insert");
        // check non-editable field
        walkProperties(model, new PropertyWalker() {
            @Override
            public void walk(Property property, Field field, CRUDField crud) {
                Object newValue;
                try {
                    newValue = PropertyUtils.getSimpleProperty(elem, property.name);
                } catch (Exception e) {
                    throw toThrowable(internalError(e, "Failed to get property %s", property.name));
                }
                // if that field is not editable, let us barf
                if (crud == null || !crud.editable())
                    checkEmpty(newValue);
            }
        });
        elem._save();
        // now get the link to the new element
        UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
        URI uri = uriBuilder.path(getClass()).path(getClass(), "get").build(elem._key());
        return created(uri);
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for editing an entity.
     * @param model the model type
     * @param id the entity id to update
     * @param elem the new values for the entity
     * @return a response with no content
     */
    public <T extends Model> Response edit(Class<T> model, Object id, final T elem) {
        Factory factory = Manager.factoryFor(model);
        @SuppressWarnings("unchecked")
        final T elemFromDB = (T) factory.findById(id);
        checkForUpdate(elem, elemFromDB);
        // copy every field
        walkProperties(model, new PropertyWalker() {
            @Override
            public void walk(Property property, Field field, CRUDField crud) {
                Object newValue;
                try {
                    newValue = PropertyUtils.getSimpleProperty(elem, property.name);
                } catch (Exception e) {
                    throw toThrowable(internalError(e, "Failed to get property %s", property.name));
                }
                // if that field is not editable, let us barf
                if (crud == null || !crud.editable())
                    checkEmpty(newValue);
                else {
                    // we can set it
                    try {
                        PropertyUtils.setSimpleProperty(elemFromDB, property.name, newValue);
                    } catch (Exception e) {
                        throw toThrowable(internalError(e, "Failed to set property %s", property.name));
                    }
                }
            }
        });
        elemFromDB._save();
        return noContent();
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for getting a list of autocomplete entries for the given field and query
     * @param model the model type
     * @param field the field to autocomplete
     * @param q the query for the autocompletion
     * @return a response with the list of autocomplete enties
     */
    public <T extends Model> Response autoComplete(Class<T> model, String field, String q) {
        checkAutoCompleteQuery(q);
        checkPermission(model, "select");
        return autoComplete(AutoComplete.getAutoComplete(model, field, q));
    }

    /**
     * Override this method to implement your own endpoint, otherwise it will be magically bound to the
     * right path and parameters for getting a descriptor for CRUD fields.
     * @param model the model type
     * @return a response with the CRUD fields descriptor
     */
    public <T extends Model> Response descriptor(Class<T> model) {
        checkPermission(model, "select");
        return Response.ok(new Descriptor<T>(model)).build();
    }

    /**
     * The CRUD fields descriptor
     *
     * @param <T> The type of entity
     * @author Stphane pardaud <stef@epardaud.fr>
     */
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.NONE)
    public static class Descriptor<T extends Model> {

        @XmlElement
        public List<Column> columns = new ArrayList<Column>();

        // For JAXB
        public Descriptor() {
        }

        public Descriptor(Class<T> model) {
            walkProperties(model, new PropertyWalker() {
                @Override
                public void walk(Property property, Field field, CRUDField crud) {
                    addColumn(property, field, crud);
                }
            });
            CRUDOrder crudOrder = model.getAnnotation(CRUDOrder.class);
            if (crudOrder != null) {
                List<Column> sortedColumns = new ArrayList<Column>();
                for (String field : crudOrder.value()) {
                    int i = findColumn(field);
                    if (i == -1)
                        throw new RuntimeException("No such field in model: " + field);
                    sortedColumns.add(columns.remove(i));
                }
                // now add any remaining column
                sortedColumns.addAll(columns);
                // and swap
                columns = sortedColumns;
            }
        }

        private int findColumn(String field) {
            for (int i = 0; i < columns.size(); i++)
                if (columns.get(i).field.equals(field))
                    return i;
            return -1;
        }

        private void addColumn(Property p, Field field, CRUDField crud) {
            // FIXME: only make it a column if it has a representation in JAXB, and allow @CRUD-less fields
            if (crud == null)
                return;
            columns.add(new Column(p, field, crud));
        }

    }

    /**
     * CRUD field descriptor
     * @author Stphane pardaud <stef@epardaud.fr>
     */
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.NONE)
    public static class Column {
        @XmlElement
        public String name;
        @XmlElement
        public String field;
        @XmlElement
        public boolean editable;
        @XmlElement
        public boolean sortable;
        @XmlElement
        public boolean autocomplete;
        @XmlElement
        public String type;
        @XmlElement
        public List<String> validators = new ArrayList<String>();

        // For JAXB
        public Column() {
        }

        public Column(Property p, Field field, CRUDField crud) {
            this.field = name = p.name;
            editable = false;
            autocomplete = false;
            sortable = false;
            Class<?> fieldType = field.getType();
            setType(fieldType);
            if (crud != null) {
                if (crud.name().length() > 0)
                    name = crud.name();
                editable = crud.editable();
                autocomplete = crud.autoComplete();
                sortable = crud.sortable();
                Type crudType = crud.type();
                if (crudType != Type.DEFAULT)
                    type = crudType.name();
            }
            addValidation(field);
        }

        private void addValidation(Field field) {
            if (field.isAnnotationPresent(NotEmpty.class) || field.isAnnotationPresent(NotNull.class))
                validators.add("notempty");
        }

        private void setType(Class<?> fieldType) {
            if (fieldType == String.class) {
                type = Type.STRING.name();
            } else if (fieldType == Integer.class || fieldType == Integer.TYPE || fieldType == Long.class
                    || fieldType == Long.TYPE || fieldType == Short.class || fieldType == Short.TYPE
                    || fieldType == BigInteger.class) {
                type = Type.INTEGER.name();
            } else if (fieldType == Float.class || fieldType == Float.TYPE || fieldType == Double.class
                    || fieldType == Double.TYPE || fieldType == BigDecimal.class) {
                type = Type.DECIMAL.name();
            } else if (fieldType == Date.class || fieldType == Calendar.class || fieldType == java.sql.Date.class
                    || fieldType == java.sql.Timestamp.class) {
                type = Type.DATE.name();
            } else if (fieldType == Boolean.class) {
                type = Type.BOOLEAN.name();
            } else
                throw new RuntimeException("Unknown field type: " + fieldType.getName());
        }
    }

    /**
     * Walks every property with CRUDFIeld
     */
    public interface PropertyWalker {
        public void walk(Property property, Field field, CRUDField crud);
    }

    /**
     * Walks every property with CRUDFIeld
     */
    protected static <T extends Model> void walkProperties(Class<T> model, PropertyWalker walker) {
        // FIXME: make this faster (10x slower than doing it manually for some reason)
        Factory factory = Manager.factoryFor(model);
        for (Property prop : factory.listProperties()) {
            Field field = prop.field;
            CRUDField crud = field.getAnnotation(CRUDField.class);
            walker.walk(prop, field, crud);
        }
    }

    /**
     * Gets the list of sortable columns for the given model
     */
    protected <T extends Model> Set<String> getSortableColumns(Class<T> model) {
        final Set<String> ret = new HashSet<String>();
        walkProperties(model, new PropertyWalker() {
            @Override
            public void walk(Property property, Field field, CRUDField crud) {
                if (crud != null && crud.sortable())
                    ret.add(property.name);
            }
        });
        return ret;
    }

    /**
     * Gets the list of searchable columns for the given model
     */
    protected <T extends Model> Set<String> getSearchableColumns(Class<T> model) {
        final Set<String> ret = new HashSet<String>();
        walkProperties(model, new PropertyWalker() {
            @Override
            public void walk(Property property, Field field, CRUDField crud) {
                if (crud != null && crud.searchable())
                    ret.add(property.name);
            }
        });
        return ret;
    }

    /**
     * Makes a paged query for the given entity
     */
    protected <T extends Model> PagedQuery<T> findPaged(Class<T> model) {
        PagedQuery<T> query = new PagedQuery<T>("FROM " + model.getSimpleName());
        query.searchFields(getSearchableColumns(model));
        return query;
    }

    /**
     * Makes a query response
     * @param <T> the entity type
     * @param q the query
     * @param results the paged query results
     * @param klass the entity type
     * @param validColumns the list of valid search columns
     * @param permissions the set of permissions to check and include in the response if the user has them
     * @return the response
     */
    protected <T> Response makeQueryResponse(DataTableQuery q, PagedQuery<T> results, Class<T> klass,
            Set<String> validColumns, UriInfo uriInfo, String... permissions) {
        return makeQueryResponse(q, results, klass, validColumns, null, uriInfo, permissions);
    }

    /**
     * Makes a query response
     * @param <T> the entity type
     * @param q the query
     * @param results the paged query results
     * @param klass the entity type
     * @param oob the OutOfBounds object to include in the response, if not null
     * @param validColumns the list of valid search columns
     * @param permissions the set of permissions to check and include in the response if the user has them
     * @return the response
     */
    protected <T> Response makeQueryResponse(DataTableQuery q, PagedQuery<T> results, Class<T> klass,
            Set<String> validColumns, Object oob, UriInfo uriInfo, String... permissions) {
        results.start = (long) q.start;
        results.limit = (long) q.length;
        if (isSortValid(q.sort, validColumns))
            results.order = q.sort;
        if (!StringUtils.isEmpty(q.search))
            results.search = q.search;
        DataTable<T> dataTable = makeDataTable(q.echo, results.getCount(), results.getResultList(), klass, oob,
                uriInfo);
        // add any permission we have
        for (String permission : permissions) {
            if (hasPermission(dataTable.facadeFor(), permission))
                dataTable.addPermission(permission);
        }
        Logger.info("Returning 200 with entity: %s", dataTable);
        return Response.ok(dataTable).build();
    }

    /**
     * Override this to make your own DataTable with the appropriate {@link @XmlSeeAlso} annotation.
     * If you do not, one will be provided for you with only T as JAXB context.
     */
    protected <T> DataTable<T> makeDataTable(String echo, long count, List<T> results, Class<T> type, Object oob,
            UriInfo uriInfo) {
        return new DataTable<T>(echo, count, results, type, oob, uriInfo);
    }

    /**
     * Checks if the given sort query is valid according to the valid columns
     * @param sort the query
     * @param validColumns the valid columns
     */
    protected boolean isSortValid(String sort, Set<String> validColumns) {
        if (sort == null)
            return false;
        final StringTokenizer tokenizer = new StringTokenizer(sort, ",");
        // we need at least one
        if (!tokenizer.hasMoreTokens())
            return false;
        while (tokenizer.hasMoreTokens()) {
            final String fragment = tokenizer.nextToken().trim();
            String field;
            if (fragment.toLowerCase().endsWith(" desc"))
                field = fragment.substring(0, fragment.length() - 5).trim();
            else if (fragment.toLowerCase().endsWith(" asc"))
                field = fragment.substring(0, fragment.length() - 4).trim();
            else
                field = fragment;
            // can be an int or an allowed name
            if (!field.matches("\\d+") && !validColumns.contains(field))
                return false;
        }
        return true;
    }

    /**
     * Logs an entity query
     */
    protected void logQuery(DataTableQuery q) {
        Logger.info("GET start: %s, length: %s, echo: %s, sort: %s, search: %s", q.start, q.length, q.echo, q.sort,
                q.search);
    }

}