/* * Copyright 2012, Ryan J. McDonough * * 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.damnhandy.uri.template; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; /** * <p> * The {@link DefaultVarExploder} is a {@link VarExploder} implementation that takes in a Java object and * extracts the properties for use in a URI Template. Given the following URI expression: * </p> * <pre> * /mapper{?address*} * </pre> * <p> * And this Java object for an address: * </p> * <pre> * Address address = new Address(); * address.setState("CA"); * address.setCity("Newport Beach"); * String result = UriTemplate.fromTemplate("/mapper{?address*}").set("address", address).expand(); * </pre> * <p> * The expanded URI will be: * </p> * <pre> * /mapper?city=Newport%20Beach&state=CA * </pre> * <p> * <p> * The {@link DefaultVarExploder} breaks down the object properties as follows: * <ul> * <li>All properties that contain a non-null return value will be included</li> * <li>Getters or fields annotated with {@link UriTransient} will <b>NOT</b> included in the list</li> * <li>By default, the property name is used as the label in the URI. This can be overridden by * placing the {@link VarName} annotation on the field or getter method and specifying a name.</li> * <li>Field level annotation take priority of getter annotations</li> * </ul> * * @author <a href="ryan@damnhandy.com">Ryan J. McDonough</a> * @version $Revision: 1.1 $ * @see VarName * @see UriTransient * @see VarExploder * @since 1.0 */ public class DefaultVarExploder implements VarExploder { /** * */ private static final String GET_PREFIX = "get"; /** * */ private static final String IS_PREIX = "is"; /** * The original object. */ private Object source; /** * The objects properties that have been extracted to a {@link Map} */ private Map<String, Object> pairs = new TreeMap<String, Object>(); /** * @param source the Object to explode */ public DefaultVarExploder(Object source) throws VarExploderException { this.setSource(source); } /** * @return the name value pairs of the input */ @Override public Map<String, Object> getNameValuePairs() { return pairs; } /** * * @param source * @throws VarExploderException */ void setSource(Object source) throws VarExploderException { this.source = source; this.initValues(); } /** * Initializes the values from the object properties and constructs a * map from those values. * * @throws VarExploderException */ private void initValues() throws VarExploderException { Class<?> c = source.getClass(); if (c.isAnnotation() || c.isArray() || c.isEnum() || c.isPrimitive()) { throw new IllegalArgumentException("The value must an object"); } if(source instanceof Map ) { this.pairs = (Map<String,Object>) source; return; } Method[] methods = c.getMethods(); for (Method method : methods) { inspectGetters(method); } scanFields(c); } /** * A lite version of the introspection logic performed by the BeanInfo introspector. * @param method */ private void inspectGetters(Method method) { String methodName = method.getName(); int prefixLength = 0; if (methodName.startsWith(GET_PREFIX)) { prefixLength = GET_PREFIX.length(); } if (methodName.startsWith(IS_PREIX)) { prefixLength = IS_PREIX.length(); } if(prefixLength == 0) { return; } String name = decapitalize(methodName.substring(prefixLength)); if(!isValidProperty(name)) { return; } // Check that the return type is not null or void Class propertyType = method.getReturnType(); if (propertyType == null || propertyType == void.class) { return; } // isXXX return boolean if (prefixLength == 2) { if (!(propertyType == boolean.class)) { return; } } // validate parameter types Class[] paramTypes = method.getParameterTypes(); if (paramTypes.length > 1 || (paramTypes.length == 1 && paramTypes[0] != int.class)) { return; } if (!method.isAnnotationPresent(UriTransient.class) && !"class".equals(name)) { Object value = getValue(method); if (method.isAnnotationPresent(VarName.class)) { name = method.getAnnotation(VarName.class).value(); } if (value != null) { pairs.put(name, value); } } } private static boolean isValidProperty(String propertyName) { return (propertyName != null) && (propertyName.length() != 0); } static String decapitalize(String name) { if (name == null) return null; // The rule for decapitalize is that: // If the first letter of the string is Upper Case, make it lower case // UNLESS the second letter of the string is also Upper Case, in which case no // changes are made. if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) { return name; } char[] chars = name.toCharArray(); chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } /** * Scans the fields on the class or super classes to look for * field-level annotations. * * @param c */ private void scanFields(Class<?> c) { if (!c.isInterface()) { Field[] fields = c.getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); if (pairs.containsKey(fieldName)) { if (field.isAnnotationPresent(UriTransient.class)) { pairs.remove(fieldName); } else if (field.isAnnotationPresent(VarName.class)) { String name = field.getAnnotation(VarName.class).value(); pairs.put(name, pairs.get(fieldName)); pairs.remove(fieldName); } } } } /* * We still need to scan the fields of the super class if its * not Object to check for annotations. There might be a better * way to do this. */ if (!c.getSuperclass().equals(Object.class)) { scanFields(c.getSuperclass()); } } /** * Return the value of the property. * * @param method * @return * @throws VarExploderException */ private Object getValue(Method method) throws VarExploderException { try { if (method == null) { return null; } return method.invoke(source); } catch (IllegalArgumentException e) { throw new VarExploderException(e); } catch (IllegalAccessException e) { throw new VarExploderException(e); } catch (InvocationTargetException e) { throw new VarExploderException(e); } } @Override public Collection<Object> getValues() throws VarExploderException { return pairs.values(); } }