/************************************************************************* * Copyright 2009-2016 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. * * This file may incorporate work covered under the following copyright * and permission notice: * * Software License Agreement (BSD License) * * Copyright (c) 2008, Regents of the University of California * All rights reserved. * * Redistribution and use of this software in source and binary forms, * with or without modification, are permitted provided that the * following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE * THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL, * COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE, * AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING * IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA, * SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY, * WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION, * REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO * IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT * NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS. ************************************************************************/ package com.eucalyptus.ws.protocol; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.log4j.Logger; import com.eucalyptus.binding.Binding; import com.eucalyptus.binding.BindingElementNotFoundException; import com.eucalyptus.binding.BindingException; import com.eucalyptus.binding.BindingManager; import com.eucalyptus.binding.HttpEmbedded; import com.eucalyptus.binding.HttpEmbeddeds; import com.eucalyptus.binding.HttpParameterMapping; import com.eucalyptus.binding.HttpParameterMappings; import com.eucalyptus.binding.HttpValue; import com.eucalyptus.crypto.util.Timestamps; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.ws.StackConfiguration; import com.eucalyptus.ws.handlers.RestfulMarshallingHandler; import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import edu.ucsb.eucalyptus.msgs.BaseData; import edu.ucsb.eucalyptus.msgs.BaseMessage; import edu.ucsb.eucalyptus.msgs.EucalyptusData; import edu.ucsb.eucalyptus.msgs.EucalyptusMessage; import groovy.lang.GroovyObject; public class BaseQueryBinding<T extends Enum<T>> extends RestfulMarshallingHandler { private static Logger LOG = Logger.getLogger( BaseQueryBinding.class ); private final UnknownParameterStrategy unknownParameterStrategy; private final T operationParam; private final List<T> altOperationParams; private final List<T> possibleParams; public enum UnknownParameterStrategy { /** * Ignore unknown parameters */ IGNORE, /** * Fail with a binding error for unknown parameters */ ERROR, ; } /** * @param namespacePattern - the format string to be used when constructing the namespace. this * can be a fully formed namespace. * @param defaultVersion - default version to use if binding problems are encountered (e.g., * unknown request namespace). * @param operationParam - this argument is used to determine the list of possible operation * parameters * @param alternativeOperationParam - these arguments are treated as alternatives to * <tt>operationParam</tt> (e.g., <tt>Action</tt> is an alternative to <tt>Operation</tt> * ). */ @SafeVarargs public BaseQueryBinding( final String namespacePattern, final String defaultVersion, final T operationParam, final T... alternativeOperationParam ) { this( namespacePattern, defaultVersion, UnknownParameterStrategy.IGNORE, operationParam, alternativeOperationParam ); } /** * @param namespacePattern - the format string to be used when constructing the namespace. this * can be a fully formed namespace. * @param defaultVersion - default version to use if binding problems are encountered (e.g., * unknown request namespace). * @param operationParam - this argument is used to determine the list of possible operation * parameters * @param alternativeOperationParam - these arguments are treated as alternatives to * <tt>operationParam</tt> (e.g., <tt>Action</tt> is an alternative to <tt>Operation</tt> * ). */ @SafeVarargs public BaseQueryBinding( final String namespacePattern, final String defaultVersion, final UnknownParameterStrategy unknownParameterStrategy, final T operationParam, final T... alternativeOperationParam ) { super( namespacePattern, defaultVersion ); this.unknownParameterStrategy = unknownParameterStrategy; this.operationParam = operationParam; this.altOperationParams = Arrays.asList( alternativeOperationParam ); this.possibleParams = Arrays.asList( operationParam.getDeclaringClass( ).getEnumConstants( ) ); } private String extractOperationName( final MappingHttpRequest httpRequest ) { if ( httpRequest.getParameters( ).containsKey( this.operationParam.toString( ) ) ) { return httpRequest.getParameters( ).get( this.operationParam.toString( ) ); } else { for ( final T param : this.altOperationParams ) { if ( httpRequest.getParameters( ).containsKey( param.toString( ) ) ) { return httpRequest.getParameters( ).get( param.toString( ) ); } } } LOG.error( "Failed to find operation parameter an " + Lists.asList( this.operationParam, this.altOperationParams.toArray( ) ).toString( ) + " in HTTP request: " + httpRequest ); return null; } @Override public Object bind( final MappingHttpRequest httpRequest ) throws BindingException { final String operationName = this.extractOperationName( httpRequest ); final String operationNameType = operationName + "Type"; for ( final T op : this.possibleParams ) httpRequest.getParameters( ).remove( op.name( ) ); final Map<String, String> params = httpRequest.getParameters( ); BaseMessage eucaMsg; Map<String, String> fieldMap; Binding currentBinding; try { currentBinding = getBindingWithElementClass( operationName ); Class<?> targetType = currentBinding==null ? null : currentBinding.getElementClass( operationName ); if ( currentBinding == null ) { currentBinding = getBindingWithElementClass( operationNameType ); targetType = currentBinding==null ? null : currentBinding.getElementClass( operationNameType ); } if ( currentBinding == null ) { //this will necessarily fault. targetType = this.getBinding( ).getElementClass( operationName ); } fieldMap = this.buildFieldMap( targetType ); eucaMsg = ( BaseMessage ) targetType.newInstance( ); } catch ( final BindingException e ) { LOG.debug( "Failed to construct message of type: " + operationName, e instanceof BindingElementNotFoundException ? null : e ); throw e; } catch ( final Exception e ) { throw new BindingException( "Failed to construct message of type " + operationName, e ); } final List<String> failedMappings = this.populateObject( ( GroovyObject ) eucaMsg, fieldMap, params ); if ( isStrictBinding( ) && ( !failedMappings.isEmpty( ) || !params.isEmpty( ) ) ) { final StringBuilder errMsg = new StringBuilder( "Failed to bind the following fields:\n" ); for ( final String f : failedMappings ) errMsg.append( f ).append( '\n' ); for ( final Map.Entry<String, String> f : params.entrySet( ) ) errMsg.append( f.getKey( ) ).append( " = " ).append( f.getValue( ) ).append( '\n' ); throw new BindingException( errMsg.toString( ) ); } validateBinding( currentBinding, operationName, params, eucaMsg ); return eucaMsg; } protected Binding getBindingWithElementClass( final String operationName ) throws BindingException { Binding binding = null; if ( this.getBinding( ).hasElementClass( operationName ) ) { binding = this.getBinding( ); } else if ( this.getDefaultBinding().hasElementClass( operationName ) ) { binding = this.getDefaultBinding(); } else if ( BindingManager.getDefaultBinding().hasElementClass( operationName ) ) { binding = BindingManager.getDefaultBinding(); } return binding; } protected void validateBinding( final Binding currentBinding, final String operationName, final Map<String, String> params, final BaseMessage eucaMsg ) throws BindingException { try { currentBinding.toOM( eucaMsg, this.getNamespace( ) ); } catch ( final RuntimeException e ) { LOG.error( "Falling back to default (unvalidated) binding for: " + operationName + " with params=" + params ); LOG.error( "Failed to build a valid message: " + e.getMessage( ), e ); try { BindingManager.getDefaultBinding().toOM( eucaMsg, BindingManager.defaultBindingNamespace( ) ); } catch ( final RuntimeException ex ) { throw new BindingException( "Default binding failed to build a valid message: " + ex.getMessage( ), ex ); } } } private boolean isStrictBinding( ) { final String strategy = StackConfiguration.UNKNOWN_PARAMETER_HANDLING; return "error".equalsIgnoreCase( strategy ) || ( !"ignore".equalsIgnoreCase( strategy ) && unknownParameterStrategy == UnknownParameterStrategy.ERROR ); } private static Field getRecursiveField( Class<?> clazz, final String fieldName ) throws Exception { Exception e = null; while ( !BaseMessage.class.equals( clazz ) && !Object.class.equals( clazz ) ) { try { return clazz.getDeclaredField( fieldName ); } catch ( final Exception e1 ) { e = e1; } clazz = clazz.getSuperclass( ); } if ( e == null ) throw new Exception("Class not supported: " + clazz); throw e; } private List<String> populateObject( final GroovyObject obj, final Map<String, String> paramFieldMap, final Map<String, String> params ) { final List<String> failedMappings = new ArrayList<String>( ); for ( final Map.Entry<String, String> e : paramFieldMap.entrySet( ) ) { try { if ( getRecursiveField( obj.getClass( ), e.getValue( ) ).getType( ).equals( ArrayList.class ) ) { failedMappings.addAll( this.populateObjectList( obj, e, params, params.size( ) ) ); } } catch ( final Exception e1 ) { LOG.debug( "Failed mapping : ", e1 ); failedMappings.add( e.getKey( ) ); } } for ( final Map.Entry<String, String> e : paramFieldMap.entrySet( ) ) { Field field = null; Class<?> declaredType = null; try { field = getRecursiveField( obj.getClass( ), e.getValue( ) ); declaredType = field.getType( ); } catch ( final Exception e2 ) { LOG.debug( "Field not found: " + e.getValue(), e2 ); } if ( params.containsKey( e.getKey( ) ) && ( declaredType == null || !EucalyptusData.class.isAssignableFrom( declaredType ) ) && !this.populateObjectField( obj, e, params ) ) { failedMappings.add( e.getKey( ) ); } else if ( ( declaredType != null ) && EucalyptusData.class.isAssignableFrom( declaredType ) ) { try { final Map<String, String> fieldMap = this.buildFieldMap( declaredType ); final Object newInstance = declaredType.newInstance( ); Map<String, String> subParams = Maps.newHashMap( ); HttpEmbedded httpEmbedded = null; if ( field != null && ( field.isAnnotationPresent( HttpEmbedded.class ) || field.isAnnotationPresent( HttpEmbeddeds.class ) ) ) { httpEmbedded = getHttpEmbeddedAnnotation( field ); } if ( httpEmbedded != null && !httpEmbedded.multiple( ) ) { subParams = params; } else { for ( final String item : Sets.newHashSet( params.keySet( ) ) ) { if ( item.startsWith( e.getKey( ) + "." ) || (item.equals( e.getKey( ) ) && isValueObject( declaredType ) )) { subParams.put( replaceStringPrefixIfExists(item, e.getKey( ) + ".", "" ), params.remove( item ) ); } } } if ( !subParams.isEmpty( ) ) { if ( httpEmbedded == null && subParams.size( ) == 1 && subParams.keySet( ).contains( e.getKey( ) ) ) { try { if ( populateValue( declaredType, (GroovyObject) newInstance, Iterables.getOnlyElement( subParams.values( ) ) ).isEmpty( ) ) { obj.setProperty( e.getValue( ), newInstance ); subParams.clear( ); } } catch ( final IllegalArgumentException e2 ) { /*param not bound error occurs for this failure*/ } if ( subParams != params ) for ( Map.Entry<String, String> entry : subParams.entrySet( ) ) { params.put( entry.getKey( ), entry.getValue( ) ); } } else { this.populateObject( (GroovyObject) newInstance, fieldMap, subParams ); obj.setProperty( e.getValue( ), newInstance ); if ( subParams != params ) for ( Map.Entry<String, String> entry : subParams.entrySet( ) ) { params.put( e.getKey( ) + "." + entry.getKey( ), entry.getValue( ) ); } } } else if ( params.containsKey( e.getKey( ) ) ) { obj.setProperty( e.getValue(), newInstance ); } } catch ( final Exception e1 ) { LOG.debug( "Error binding object", e1 ); } } else { failedMappings.remove( e.getKey( ) ); } } return failedMappings; } @SuppressWarnings( "unchecked" ) private boolean populateObjectField( final GroovyObject obj, final Map.Entry<String, String> paramFieldPair, final Map<String, String> params ) { try { final Class<?> declaredType = getRecursiveField( obj.getClass( ), paramFieldPair.getValue( ) ).getType( ); final Object value = convertToType( new Supplier<String>(){ @Override public String get() { return params.remove( paramFieldPair.getKey() ); } }, declaredType ); if ( value != null ) obj.setProperty( paramFieldPair.getValue( ), value ); return !params.containsKey( paramFieldPair.getKey() ); } catch ( final Exception e1 ) { return false; } } private Object convertToType( final Supplier<String> value, final Class<?> targetType ) throws Exception { if ( targetType.equals( String.class ) ) return value.get(); else if ( targetType.getName( ).equals( "int" ) ) return Integer.parseInt( value.get() ); else if ( targetType.equals( Integer.class ) ) return Integer.valueOf( value.get() ); else if ( targetType.getName( ).equals( "boolean" ) ) return Boolean.parseBoolean( value.get() ); else if ( targetType.equals( Boolean.class ) ) return Boolean.valueOf( value.get() ); else if ( targetType.getName( ).equals( "long" ) ) return Long.parseLong( value.get() ); else if ( targetType.equals( Long.class ) ) return Long.valueOf( value.get() ); else if ( targetType.getName( ).equals( "double" ) ) return Double.parseDouble( value.get() ); else if ( targetType.equals( Double.class ) ) return Double.valueOf( value.get() ); else if ( targetType.equals( Date.class ) ) return Timestamps.parseIso8601Timestamp( value.get() ); else return null; } @SuppressWarnings( "rawtypes" ) private List<String> populateObjectList( final GroovyObject obj, final Map.Entry<String, String> paramFieldPair, final Map<String, String> params, final int paramSize ) { final List<String> failedMappings = new ArrayList<String>( ); try { final Field declaredField = getRecursiveField( obj.getClass( ), paramFieldPair.getValue( ) ); final ArrayList theList = ( ArrayList ) obj.getProperty( paramFieldPair.getValue( ) ); final Class genericType = ( Class ) ( ( ParameterizedType ) declaredField.getGenericType( ) ).getActualTypeArguments( )[0]; // :: simple case: FieldName.# ::// if ( String.class.equals( genericType ) || Boolean.class.equals( genericType ) || Integer.class.equals( genericType ) || Long.class.equals( genericType ) || Double.class.equals( genericType ) || Date.class.equals( genericType ) ) { if ( params.containsKey( paramFieldPair.getKey( ) ) ) { theList.add( convertToType( Suppliers.ofInstance(params.remove( paramFieldPair.getKey() )), genericType ) ); } else { final List<String> keys = Lists.newArrayList( params.keySet( ) ); final Pattern paramPattern = Pattern.compile( Pattern.quote(paramFieldPair.getKey( )) + "\\.([0-9]{1,7})" ); final Map<String,Object> indexToValueMap = new TreeMap<String,Object>( Ordering.natural().onResultOf( FunctionToInteger.INSTANCE ) ); for ( final String k : keys ) { final Matcher matcher = paramPattern.matcher( k ); if ( matcher.matches() ) { indexToValueMap.put( matcher.group(1), convertToType( Suppliers.ofInstance(params.remove( k )), genericType ) ); } } theList.addAll( indexToValueMap.values() ); } } else if ( declaredField.isAnnotationPresent( HttpEmbedded.class ) || declaredField.isAnnotationPresent( HttpEmbeddeds.class )) { final HttpEmbedded annoteEmbedded = getHttpEmbeddedAnnotation( declaredField ); // :: build the parameter map and call populate object recursively ::// if ( annoteEmbedded.multiple( ) ) { final List<String> keys = Lists.newArrayList( params.keySet( ) ); final Map<String,Map<String,String>> subParamMaps = new TreeMap<>( Ordering.natural().onResultOf( FunctionToInteger.INSTANCE ) ); final Map<String,String> valueMap = new TreeMap<>( Ordering.natural().onResultOf( FunctionToInteger.INSTANCE ) ); for ( final String k : keys ) { if ( k.matches( Pattern.quote( paramFieldPair.getKey( ) ) + "\\.[0-9]{1,7}\\..*" ) ) { final String currentValue = params.remove( k ); final String setKey = k.replaceAll( "^"+ paramFieldPair.getKey( ) + "\\.([0-9]{1,7})\\..*", "$1" ); if ( setKey.length() > 7 ) continue; final String subKey = k.replaceAll( "^"+ paramFieldPair.getKey( ) + "\\.[0-9]{1,7}\\." , "" ); Map<String,String> subMap = subParamMaps.get( setKey ); if ( subMap == null ) { subParamMaps.put( setKey, subMap = Maps.newHashMap() ); } subMap.put( subKey, currentValue ); } else if ( k.matches( Pattern.quote( paramFieldPair.getKey( ) ) + "\\.[0-9]{1,7}" ) ) { final String currentValue = params.remove( k ); final String orderKey = k.replaceAll( "^"+ paramFieldPair.getKey( ) + "\\.([0-9]{1,7})", "$1" ); if ( orderKey.length() > 7 ) continue; valueMap.put( orderKey, currentValue ); } } if ( subParamMaps.isEmpty( ) ) { for ( final String value : valueMap.values() ) { failedMappings.addAll( this.populateEmbedded( genericType, value, theList ) ); } } else { for ( final Map<String,String> subParams : subParamMaps.values() ) { failedMappings.addAll( this.populateEmbedded( genericType, subParams, theList ) ); } } } else { failedMappings.addAll( this.populateEmbedded( genericType, params, theList ) ); } } } catch ( final Exception e1 ) { LOG.debug( "FAILED HERE : ", e1 ); failedMappings.add( paramFieldPair.getKey( ) ); } return failedMappings; } private List<String> populateEmbedded( final Class<?> genericType, final Map<String, String> params, @SuppressWarnings( "rawtypes" ) final ArrayList theList ) throws InstantiationException, IllegalAccessException { final GroovyObject embedded = ( GroovyObject ) genericType.newInstance( ); final Map<String, String> embeddedFields = this.buildFieldMap( genericType ); final int startSize = params.size( ); final List<String> embeddedFailures = this.populateObject( embedded, embeddedFields, params ); if ( embeddedFailures.isEmpty( ) && !( params.size( ) - startSize == 0 ) ) theList.add( embedded ); return embeddedFailures; } private List<String> populateEmbedded( final Class<?> genericType, final String value, @SuppressWarnings( "rawtypes" ) final ArrayList theList ) throws InstantiationException, IllegalAccessException { final GroovyObject embedded = ( GroovyObject ) genericType.newInstance( ); final List<String> embeddedFailures = populateValue( genericType, embedded, value ); if ( embeddedFailures.isEmpty( ) ) { theList.add( embedded ); } return embeddedFailures; } private List<String> populateValue( final Class<?> genericType, final GroovyObject targetObject, final String value ) throws InstantiationException, IllegalAccessException { final Field valueField = this.findValueField( genericType ); if ( valueField == null ) { throw new IllegalArgumentException( "Simple type cannot be mapped for " + genericType.getSimpleName( ) ); } final List<String> embeddedFailures = this.populateObject( targetObject, Maps.newHashMap( Collections.singletonMap( "value", valueField.getName() ) ), Maps.newHashMap( Collections.singletonMap( "value", value ) ) ); return embeddedFailures; } @Nullable private Field findValueField( Class<?> targetType ) { while ( !BaseMessage.class.equals( targetType ) && !EucalyptusMessage.class.equals( targetType ) && !EucalyptusData.class.equals( targetType ) && !BaseData.class.equals( targetType ) ) { final Field[] fields = targetType.getDeclaredFields( ); for ( final Field f : fields ) { if ( Modifier.isStatic( f.getModifiers( ) ) ) { continue; } else if ( f.isAnnotationPresent( HttpValue.class ) ) { return f; } } targetType = targetType.getSuperclass( ); } return null; } private boolean isValueObject( final Class<?> targetType ) { return findValueField( targetType ) != null; } private Map<String, String> buildFieldMap( Class<?> targetType ) { final Map<String, String> fieldMap = new HashMap<String, String>( ); while ( !BaseMessage.class.equals( targetType ) && !EucalyptusMessage.class.equals( targetType ) && !EucalyptusData.class.equals( targetType ) && !BaseData.class.equals( targetType ) ) { final Field[] fields = targetType.getDeclaredFields( ); for ( final Field f : fields ) { if ( Modifier.isStatic( f.getModifiers( ) ) ) continue; else if ( f.isAnnotationPresent( HttpParameterMapping.class ) || f.isAnnotationPresent( HttpParameterMappings.class ) ) { for ( String parameter : getHttpParameterMappingAnnotation( f ).parameter() ) { fieldMap.put( parameter, f.getName( ) ); } } else { fieldMap.put( f.getName( ).substring( 0, 1 ).toUpperCase( ).concat( f.getName( ).substring( 1 ) ), f.getName( ) ); } } targetType = targetType.getSuperclass( ); } return fieldMap; } private HttpEmbedded getHttpEmbeddedAnnotation( final Field field ) { if ( field.isAnnotationPresent( HttpEmbedded.class ) ) { return field.getAnnotation( HttpEmbedded.class ); } else { return getVersionedAnnotation( field.getAnnotation( HttpEmbeddeds.class ).value(), HttpEmbeddedVersionExtractor.INSTANCE ); } } private HttpParameterMapping getHttpParameterMappingAnnotation( final Field field ) { if ( field.isAnnotationPresent( HttpParameterMapping.class ) ) { return field.getAnnotation( HttpParameterMapping.class ); } else { return getVersionedAnnotation( field.getAnnotation( HttpParameterMappings.class ).value(), HttpParameterMappingVersionExtractor.INSTANCE ); } } private <T extends Annotation> T getVersionedAnnotation( final T[] values, final Function<T,String> versionExtractor ) { for ( final T t : values ) { final String version = versionExtractor.apply( t ); if ( Strings.isNullOrEmpty( version ) ) continue; if ( getNamespace().compareTo( getNamespaceForVersion( version ) ) < 1 ) { return t; } } return values[ values.length - 1 ]; } private enum HttpEmbeddedVersionExtractor implements Function<HttpEmbedded,String> { INSTANCE; @Override public String apply( final HttpEmbedded httpEmbedded ) { return httpEmbedded.version(); } } private enum HttpParameterMappingVersionExtractor implements Function<HttpParameterMapping,String> { INSTANCE; @Override public String apply( final HttpParameterMapping httpParameterMapping ) { return httpParameterMapping.version(); } } private enum FunctionToInteger implements Function<String,Integer> { INSTANCE { @Override public Integer apply(final String parameterIndex ) { Integer result = Integer.MAX_VALUE; try { result = Integer.valueOf( parameterIndex ); } catch ( NumberFormatException nfe ) { // use default } return result; } } } public static String replaceStringPrefixIfExists(String target, String oldPrefix, String newPrefix) { if (target == null) throw new NullPointerException("target can not be null"); if (oldPrefix == null) throw new NullPointerException("oldPrefix can not be null"); if (newPrefix == null) throw new NullPointerException("newPrefix can not be null"); return target.startsWith(oldPrefix) ? newPrefix + target.substring(oldPrefix.length()) : target; } }