com.ocpsoft.pretty.faces.config.annotation.PrettyAnnotationHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.ocpsoft.pretty.faces.config.annotation.PrettyAnnotationHandler.java

Source

/*
 * Copyright 2010 Lincoln Baxter, III
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ocpsoft.pretty.faces.config.annotation;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.ocpsoft.pretty.faces.annotation.URLAction;
import com.ocpsoft.pretty.faces.annotation.URLAction.PhaseId;
import com.ocpsoft.pretty.faces.annotation.URLActions;
import com.ocpsoft.pretty.faces.annotation.URLBeanName;
import com.ocpsoft.pretty.faces.annotation.URLMapping;
import com.ocpsoft.pretty.faces.annotation.URLMappings;
import com.ocpsoft.pretty.faces.annotation.URLQueryParameter;
import com.ocpsoft.pretty.faces.annotation.URLValidator;
import com.ocpsoft.pretty.faces.config.PrettyConfigBuilder;
import com.ocpsoft.pretty.faces.config.mapping.PathValidator;
import com.ocpsoft.pretty.faces.config.mapping.QueryParameter;
import com.ocpsoft.pretty.faces.config.mapping.UrlAction;
import com.ocpsoft.pretty.faces.config.mapping.UrlMapping;
import com.ocpsoft.pretty.faces.el.ConstantExpression;
import com.ocpsoft.pretty.faces.el.LazyBeanNameFinder;
import com.ocpsoft.pretty.faces.el.LazyExpression;
import com.ocpsoft.pretty.faces.el.PrettyExpression;

@SuppressWarnings("unchecked")
public class PrettyAnnotationHandler {

    /**
     * The logger
     */
    private static final Log log = LogFactory.getLog(PrettyAnnotationHandler.class);

    /**
     * A map assigning mapping IDs to {@link UrlMapping} instances
     */
    private final Map<String, UrlMapping> urlMappings = new HashMap<String, UrlMapping>();

    /**
     * A map to resolve the bean name for a {@link Class} object
     */
    private final Map<Class, String> beanNameMap = new HashMap<Class, String>();

    /**
     * The {@link ActionSpec} objects generated from annotation scanning
     */
    private final List<ActionSpec> urlActions = new ArrayList<ActionSpec>();

    /**
     * The {@link QueryParamSpec} objects generated from annotation scanning
     */
    private final List<QueryParamSpec> queryParamList = new ArrayList<QueryParamSpec>();

    /**
     * Reference to the {@link LazyBeanNameFinder}
     */
    private final LazyBeanNameFinder beanNameFinder;

    /**
     * Constructor
     */
    public PrettyAnnotationHandler(LazyBeanNameFinder beanNameFinder) {
        this.beanNameFinder = beanNameFinder;
    }

    /**
     * This method scans the supplied class for PrettyFaces annotations. The
     * method must be called for every class that should be scanner before
     * finally calling {@link #build(PrettyConfigBuilder)}.
     * 
     * @param clazz The class to scan
     */
    public void processClass(Class clazz) {

        // log class name on trace level
        if (log.isTraceEnabled()) {
            log.trace("Analyzing class: " + clazz.getName());
        }

        try {

            // scan for PrettyAnnotation class
            // returns the mapping ID, if an annotation was found
            String[] classMappingIds = processClassMappingAnnotations(clazz);

            // scan for PrettyBean annotation
            processPrettyBeanAnnotation(clazz);

            // process annotations on public methods
            for (Method method : clazz.getMethods()) {
                processMethodAnnotations(method, classMappingIds);
            }

            // loop over fields to find URLQueryParameter annotations
            for (Field field : clazz.getDeclaredFields()) {
                processFieldAnnotations(field, classMappingIds);
            }

        } catch (NoClassDefFoundError e) {
            // reference to another class unknown to the classloader
            log.debug("Unable to process class '" + clazz.getName() + "': " + e.toString());
        }

    }

    /**
     * Checks for PrettyFaces mapping annotations on a single class
     * 
     * @param clazz
     *           Class to scan
     * @return The IDs of the mappings found on the class
     */
    public String[] processClassMappingAnnotations(Class clazz) {

        // list of all mapping IDs found on the class
        List<String> classMappingIds = new ArrayList<String>();

        // get reference to @URLMapping annotation
        URLMapping mappingAnnotation = (URLMapping) clazz.getAnnotation(URLMapping.class);

        // process annotation if it exists
        if (mappingAnnotation != null) {
            String mappingId = processPrettyMappingAnnotation(clazz, mappingAnnotation);
            classMappingIds.add(mappingId);
        }

        // container annotation
        URLMappings mappingsAnnotation = (URLMappings) clazz.getAnnotation(URLMappings.class);

        if (mappingsAnnotation != null) {

            // process all contained @URLMapping annotations
            for (URLMapping child : mappingsAnnotation.mappings()) {
                String mappingId = processPrettyMappingAnnotation(clazz, child);
                classMappingIds.add(mappingId);
            }

        }

        // return list of mappings found
        return classMappingIds.toArray(new String[classMappingIds.size()]);

    }

    /**
     * Process a single {@link URLMapping} annotation.
     * 
     * @param clazz The class that the annotation was found on
     * @param mappingAnnotation The annotation to process
     * @return The mapping ID of the mapping found
     */
    private String processPrettyMappingAnnotation(Class clazz, URLMapping mappingAnnotation) {

        // log class name
        if (log.isTraceEnabled()) {
            log.trace("Found @URLMapping annotation on class: " + clazz.getName());
        }

        // create UrlMapping from annotation
        UrlMapping mapping = new UrlMapping();
        mapping.setId(mappingAnnotation.id());
        mapping.setParentId(mappingAnnotation.parentId());
        mapping.setPattern(mappingAnnotation.pattern());
        mapping.setViewId(mappingAnnotation.viewId());
        mapping.setOutbound(mappingAnnotation.outbound());
        mapping.setOnPostback(mappingAnnotation.onPostback());

        // register mapping
        Object existingMapping = urlMappings.put(mapping.getId(), mapping);

        // fail if a mapping with this ID already existed
        if (existingMapping != null) {
            throw new IllegalArgumentException("Duplicated mapping id: " + mapping.getId());
        }

        // At bean name to lookup map if it has been specified
        if ((mappingAnnotation.beanName() != null) && (mappingAnnotation.beanName().length() > 0)) {
            beanNameMap.put(clazz, mappingAnnotation.beanName());
        }

        // process validations
        for (URLValidator validationAnnotation : mappingAnnotation.validation()) {

            // index attribute is required in this case
            if (validationAnnotation.index() < 0) {
                throw new IllegalArgumentException(
                        "Please set the index of the path parameter you want to validate with the @URLValidator specified on mapping: "
                                + mapping.getId());
            }

            // prepare PathValidator
            PathValidator pathValidator = new PathValidator();
            pathValidator.setIndex(validationAnnotation.index());
            pathValidator.setOnError(validationAnnotation.onError());
            pathValidator.setValidatorIds(join(validationAnnotation.validatorIds(), " "));

            // optional validator method
            if (!isBlank(validationAnnotation.validator())) {
                pathValidator.setValidatorExpression(new ConstantExpression(validationAnnotation.validator()));
            }

            // add PathValidator to the mapping
            mapping.getPathValidators().add(pathValidator);

        }

        // return mapping id
        return mapping.getId().trim();

    }

    /**
     * Checks the class for a {@link URLBeanName} annotation.
     * 
     * @param clazz Class to scan
     */
    private void processPrettyBeanAnnotation(Class clazz) {

        // get reference to @URLMapping annotation
        URLBeanName prettyBean = (URLBeanName) clazz.getAnnotation(URLBeanName.class);

        // process annotation if it exists
        if (prettyBean != null) {

            // log class name
            if (log.isTraceEnabled()) {
                log.trace("Found @URLBeanName annotation on class: " + clazz.getName());
            }

            // add bean to map
            beanNameMap.put(clazz, prettyBean.value());

        }

    }

    /**
     * Searches for {@link URLAction} or {@link URLActions} annotations on a
     * method.
     * 
     * @param method Method to scan
     * @param classMappingIds The mapping IDs of the class this method belongs to
     */
    private void processMethodAnnotations(Method method, String[] classMappingIds) {

        // is there a @URLAction annotation on the class?
        URLAction actionAnnotation = method.getAnnotation(URLAction.class);
        if (actionAnnotation != null) {
            processPrettyActionAnnotation(actionAnnotation, method, classMappingIds);
        }

        // is there a @URLAction container annotation on the class?
        URLActions actionsAnnotation = method.getAnnotation(URLActions.class);
        if (actionsAnnotation != null) {
            // process all @URLAction annotations
            for (URLAction child : actionsAnnotation.actions()) {
                processPrettyActionAnnotation(child, method, classMappingIds);
            }
        }
    }

    /**
     * Searches for {@link URLQueryParameter} annotations on a single field.
     * 
     * @param field Field to scan
     * @param classMappingIds The mapping IDs of the class this method belongs to
     */
    private void processFieldAnnotations(Field field, String[] classMappingIds) {
        // Is there a @URLQueryParameter annotation?
        URLQueryParameter queryParamAnnotation = field.getAnnotation(URLQueryParameter.class);

        if (queryParamAnnotation != null) {

            // create a QueryParamSpec from the annotation
            QueryParamSpec queryParam = new QueryParamSpec();
            queryParam.setFieldName(field.getName());
            queryParam.setOwnerClass(field.getDeclaringClass());
            queryParam.setName(queryParamAnnotation.value());
            queryParam.setOnPostback(queryParamAnnotation.onPostback());

            // check which mapping the action belongs to
            if (!isBlank(queryParamAnnotation.mappingId())) {
                // action belongs to the mapping mentioned with mappingId attribute
                queryParam.setMappingIds(new String[] { queryParamAnnotation.mappingId().trim() });
            } else if (classMappingIds != null && classMappingIds.length > 0) {
                // use the mappings found on the class
                queryParam.setMappingIds(classMappingIds);
            } else {
                throw new IllegalArgumentException("Unable to find a suitable mapping "
                        + "for the query-parameter definied on field '" + field.getName() + "' in class '"
                        + field.getDeclaringClass().getName() + "'. Either place a @URLMapping annotation on the "
                        + "class or reference a foreign mapping using the 'mappingId' attribute.");
            }

            // check if there is also a validation annotation placed on the field
            URLValidator validationAnnotation = field.getAnnotation(URLValidator.class);

            // check if annotation has been found
            if (validationAnnotation != null) {

                // set validation options on the QueryParamSpec object
                queryParam.setValidatorIds(validationAnnotation.validatorIds());
                queryParam.setOnError(validationAnnotation.onError());
                queryParam.setValidator(validationAnnotation.validator());

            }

            // add the new spec object to the list of specs
            queryParamList.add(queryParam);

        }

    }

    /**
     * Creates a {@link UrlAction} object from the supplied {@link URLAction}
     * annotation
     * 
     * @param actionAnnotation The annotation
     * @param method The method that was annotated
     * @param classMappingIds the mapping IDs of the current class
     */
    private void processPrettyActionAnnotation(URLAction actionAnnotation, Method method,
            String[] classMappingIds) {

        // Create ActionSpec
        ActionSpec actionSpec = new ActionSpec();
        actionSpec.setMethod(method);
        actionSpec.setOnPostback(actionAnnotation.onPostback());
        actionSpec.setInheritable(actionAnnotation.inheritable());
        actionSpec.setPhaseId(actionAnnotation.phaseId());

        // check which mapping the action belongs to
        if (!isBlank(actionAnnotation.mappingId())) {
            // action belongs to the mapping mentioned with mappingId attribute
            actionSpec.setMappingIds(new String[] { actionAnnotation.mappingId().trim() });
        } else if (classMappingIds != null && classMappingIds.length > 0) {
            // use the mapping found on the class
            actionSpec.setMappingIds(classMappingIds);
        } else {
            // No mapping found... throw an exception..
            throw new IllegalArgumentException("Unable to find a suitable mapping "
                    + "for the action definied on method '" + method.getName() + "' in class '"
                    + method.getDeclaringClass().getName() + "'. Either place a @URLMapping annotation on the "
                    + "class or reference a foreign mapping using the 'mappingId' attribute.");
        }

        // add action to list of actions
        urlActions.add(actionSpec);

    }

    /**
     * Returns <code>true</code> for "blank" strings.
     * 
     * @param str Input string
     * @return <code>true</code> if string is <code>null</code> or trimmed value
     *         is empty
     */
    private static boolean isBlank(String str) {
        return (str == null) || (str.trim().length() == 0);
    }

    /**
     * This methods adds all mappings found to the supplied
     * {@link PrettyConfigBuilder}. It should be called after all classes has
     * been scanned via {@link #processClass(Class)}.
     * 
     * @param builder The builder to add the mappings to
     */
    public void build(PrettyConfigBuilder builder) {

        // process all actions found
        for (ActionSpec actionSpec : urlActions) {

            // create an action for each referenced mapping
            for (String mappingId : actionSpec.getMappingIds()) {

                // Get the mapping references by the action
                UrlMapping mapping = urlMappings.get(mappingId);

                /*
                 * Fail for unresolved mappings. This may happen when the user places
                 * invalid mapping IDs in the mappingId attribute of
                 * 
                 * @URLAction or @URLQueryParameter
                 */
                if (mapping == null) {
                    throw new IllegalArgumentException("Unable to find the mapping '" + mappingId
                            + "' referenced at method '" + actionSpec.getMethod().getName() + "' in class '"
                            + actionSpec.getMethod().getDeclaringClass().getName() + "'.");
                }

                // build UrlMapping
                UrlAction urlAction = new UrlAction();
                urlAction.setPhaseId(actionSpec.getPhaseId());
                urlAction.setOnPostback(actionSpec.isOnPostback());
                urlAction.setInheritable(actionSpec.isInheritable());

                // try to get bean name
                Class clazz = actionSpec.getMethod().getDeclaringClass();

                // build expression
                PrettyExpression expression = buildPrettyExpression(clazz, actionSpec.getMethod().getName());
                urlAction.setAction(expression);

                // trace
                if (log.isTraceEnabled()) {
                    log.trace("Adding action expression '" + urlAction.getAction() + "' to mapping: "
                            + mapping.getId());
                }

                // register this action
                mapping.addAction(urlAction);

            }
        }

        for (QueryParamSpec queryParamSpec : queryParamList) {

            // create a query param for each referenced mapping
            for (String mappingId : queryParamSpec.getMappingIds()) {

                // Get the mapping references by the query param
                UrlMapping mapping = urlMappings.get(mappingId);

                // fail for unresolved mappings
                if (mapping == null) {
                    throw new IllegalArgumentException("Unable to find the mapping '" + mappingId
                            + "' referenced at field '" + queryParamSpec.getFieldName() + "' in class '"
                            + queryParamSpec.getOwnerClass().getName() + "'.");
                }

                // build UrlMapping
                QueryParameter queryParam = new QueryParameter();
                queryParam.setName(queryParamSpec.getName());
                queryParam.setOnError(queryParamSpec.getOnError());
                queryParam.setValidatorIds(join(queryParamSpec.getValidatorIds(), " "));
                queryParam.setOnPostback(queryParamSpec.isOnPostback());

                // optional validator method
                if (!isBlank(queryParamSpec.getValidator())) {
                    queryParam.setValidatorExpression(new ConstantExpression(queryParamSpec.getValidator()));
                }

                // try to get bean name
                Class<?> clazz = queryParamSpec.getOwnerClass();

                // build expression
                PrettyExpression expression = buildPrettyExpression(clazz, queryParamSpec.getFieldName());
                queryParam.setExpression(expression);

                // trace
                if (log.isTraceEnabled()) {
                    log.trace("Registered query-param '" + queryParam.getName() + "' to '" + expression
                            + "' in mapping: " + mapping.getId());
                }

                // register this action
                mapping.addQueryParam(queryParam);

            }

        }

        // finally register all mappings
        for (UrlMapping mapping : urlMappings.values()) {
            builder.addMapping(mapping);
        }
    }

    /**
     * Creates a {@link PrettyExpression} for a class and component. This method
     * may return a {@link ConstantExpression} or a {@link LazyExpression}.
     * 
     * @param clazz The class of the bean
     * @param component the component (property or method name)
     * @return The expression
     */
    private PrettyExpression buildPrettyExpression(Class<?> clazz, String component) {

        if (log.isTraceEnabled()) {
            log.trace("Searching name of bean: " + clazz.getName());
        }

        // get name from internal map build from @URLBeanName annotations and
        // previously resolved names
        String beanName = beanNameMap.get(clazz);

        // return a constant expression
        if (beanName != null) {
            if (log.isTraceEnabled()) {
                log.trace("Got bean name from @URLBeanName annotation: " + beanName);
            }

            return new ConstantExpression("#{" + beanName + "." + component + "}");
        }

        // build a lazy expression
        else {

            if (log.isTraceEnabled()) {
                log.trace("Name of bean not found. Building lazy expression for: " + clazz.getName());
            }

            return new LazyExpression(beanNameFinder, clazz, component);

        }

    }

    /**
     * Joins the list of values.
     * 
     * @param values values to join
     * @param separator the separator to use
     * @return joined list of values
     */
    private static String join(String[] values, String separator) {
        StringBuilder result = new StringBuilder();
        if (values != null) {
            for (int i = 0; i < values.length; i++) {
                if (i > 0) {
                    result.append(separator);
                }
                result.append(values[i]);
            }
        }
        return result.toString();
    }

    /**
     * Internal class to hold parameters of a {@link URLAction} annotation.
     * 
     * @author Christian Kaltepoth
     */
    private static class ActionSpec {

        private Method method;
        private boolean onPostback;
        private PhaseId phaseId;
        private String[] mappingIds;
        private boolean inheritable;

        public boolean isOnPostback() {
            return onPostback;
        }

        public void setOnPostback(boolean onPostback) {
            this.onPostback = onPostback;
        }

        public PhaseId getPhaseId() {
            return phaseId;
        }

        public void setPhaseId(PhaseId phaseId) {
            this.phaseId = phaseId;
        }

        public Method getMethod() {
            return method;
        }

        public void setMethod(Method method) {
            this.method = method;
        }

        public String[] getMappingIds() {
            return mappingIds;
        }

        public void setMappingIds(String[] mappingIds) {
            this.mappingIds = mappingIds;
        }

        public boolean isInheritable() {
            return inheritable;
        }

        public void setInheritable(boolean inheritable) {
            this.inheritable = inheritable;
        }

    }

    /**
     * Internal class to hold parameters of a Pretty annotation.
     * 
     * @author Christian Kaltepoth
     */
    private static class QueryParamSpec {
        private String fieldName;
        private Class<?> ownerClass;
        private String[] mappingIds;
        private String name;
        private String onError;
        private String[] validatorIds = {};
        private String validator;
        private boolean onPostback;

        public String getValidator() {
            return validator;
        }

        public void setValidator(String validator) {
            this.validator = validator;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getFieldName() {
            return fieldName;
        }

        public void setFieldName(String fieldName) {
            this.fieldName = fieldName;
        }

        public Class<?> getOwnerClass() {
            return ownerClass;
        }

        public void setOwnerClass(Class<?> ownerClass) {
            this.ownerClass = ownerClass;
        }

        public String getOnError() {
            return onError;
        }

        public void setOnError(String onError) {
            this.onError = onError;
        }

        public String[] getValidatorIds() {
            return validatorIds;
        }

        public void setValidatorIds(String[] validatorIds) {
            this.validatorIds = validatorIds;
        }

        public boolean isOnPostback() {
            return onPostback;
        }

        public void setOnPostback(boolean onPostback) {
            this.onPostback = onPostback;
        }

        public String[] getMappingIds() {
            return mappingIds;
        }

        public void setMappingIds(String[] mappingIds) {
            this.mappingIds = mappingIds;
        }
    }

}