/**
   * 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(final 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 the class for a {@link URLBeanName} annotation.
   *
   * @param clazz Class to scan
   */
  private void processPrettyBeanAnnotation(final 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());
    }
  }
  /**
   * 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(final Class<?> clazz, final 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);
    }
  }
@SuppressWarnings({"unchecked", "rawtypes"})
public class PrettyAnnotationHandler {

  /** The logger */
  private static final Logger log = Logger.getLogger(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(final 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(final 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(final 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(
      final Class clazz, final 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);
    }

    // process converters
    for (URLConverter converterAnnotation : mappingAnnotation.converter()) {

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

      // prepare PathValidator
      PathConverter pathConverter = new PathConverter();
      pathConverter.setIndex(converterAnnotation.index());
      pathConverter.setConverterId(StringUtils.trimToNull(converterAnnotation.converterId()));

      // add PathValidator to the mapping
      mapping.addPathConverter(pathConverter);
    }

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

  /**
   * Checks the class for a {@link URLBeanName} annotation.
   *
   * @param clazz Class to scan
   */
  private void processPrettyBeanAnnotation(final 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(final Method method, final 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(final Field field, final 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());
      }

      // check if there is also a converter annotation placed on the field
      URLConverter converterAnnotation = field.getAnnotation(URLConverter.class);
      if (converterAnnotation != null) {
        queryParam.setConverterId(converterAnnotation.converterId().trim());
      }

      // 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(
      final URLAction actionAnnotation, final Method method, final 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(final 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(final 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.setConverterId(StringUtils.trimToNull(queryParamSpec.getConverterId()));
        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(final Class<?> clazz, final 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(final String[] values, final 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(final boolean onPostback) {
      this.onPostback = onPostback;
    }

    public PhaseId getPhaseId() {
      return phaseId;
    }

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

    public Method getMethod() {
      return method;
    }

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

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

    public void setMappingIds(final 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 converterId;
    private String[] validatorIds = {};
    private String validator;
    private boolean onPostback;

    public String getValidator() {
      return validator;
    }

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

    public String getName() {
      return name;
    }

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

    public String getFieldName() {
      return fieldName;
    }

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

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

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

    public String getOnError() {
      return onError;
    }

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

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

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

    public boolean isOnPostback() {
      return onPostback;
    }

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

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

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

    public String getConverterId() {
      return converterId;
    }

    public void setConverterId(String converterId) {
      this.converterId = converterId;
    }
  }
}
  /**
   * 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(final 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.setConverterId(StringUtils.trimToNull(queryParamSpec.getConverterId()));
        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);
    }
  }
  /**
   * 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(
      final Class clazz, final 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);
    }

    // process converters
    for (URLConverter converterAnnotation : mappingAnnotation.converter()) {

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

      // prepare PathValidator
      PathConverter pathConverter = new PathConverter();
      pathConverter.setIndex(converterAnnotation.index());
      pathConverter.setConverterId(StringUtils.trimToNull(converterAnnotation.converterId()));

      // add PathValidator to the mapping
      mapping.addPathConverter(pathConverter);
    }

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