com.eucalyptus.ws.protocol.BaseQueryBinding.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.ws.protocol.BaseQueryBinding.java

Source

/*************************************************************************
 * Copyright 2009-2013 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.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 org.apache.log4j.Logger;
import com.eucalyptus.binding.Binding;
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.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.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.
                try {
                    targetType = this.getBinding().getElementClass(operationName);
                } catch (final BindingException ex) {
                    LOG.error(ex, ex);
                    throw ex;
                }
            }
            fieldMap = this.buildFieldMap(targetType);
            eucaMsg = (BaseMessage) targetType.newInstance();
        } catch (final BindingException e) {
            LOG.debug("Failed to construct message of type: " + operationName, e);
            LOG.error(e, 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()) {
            Class<?> declaredType = null;
            try {
                declaredType = getRecursiveField(obj.getClass(), e.getValue()).getType();
            } catch (final Exception e2) {
                LOG.debug("Field not found: " + e.getValue(), e2);
            }

            if (params.containsKey(e.getKey()) && !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();
                    final Map<String, String> subParams = Maps.newHashMap();

                    for (final String item : Sets.newHashSet(params.keySet())) {
                        if (item.startsWith(e.getKey())) {
                            params.get(item);
                            subParams.put(item.replace(e.getKey() + ".", ""), params.remove(item));
                        }
                    }
                    this.populateObject((GroovyObject) newInstance, fieldMap, subParams);
                    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<String, Map<String, String>>(
                            Ordering.natural().onResultOf(FunctionToInteger.INSTANCE));
                    for (final String k : keys) {
                        if (k.startsWith(paramFieldPair.getKey() + ".")) {
                            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);
                        }
                    }

                    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 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;
            }
        }
    }
}