/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.client.dispatcher;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.Response;
import com.google.gwt.logging.client.LogConfiguration;

import java.util.logging.Logger;

import org.fusesource.restygwt.client.Dispatcher;
import org.fusesource.restygwt.client.Method;
import org.fusesource.restygwt.client.cache.CacheKey;
import org.fusesource.restygwt.client.cache.ComplexCacheKey;
import org.fusesource.restygwt.client.cache.QueueableCacheStorage;
import org.fusesource.restygwt.client.callback.CallbackFactory;
import org.fusesource.restygwt.client.callback.FilterawareRequestCallback;

public class CachingDispatcherFilter implements DispatcherFilter {

    /**
     * one instance of {@link QueueableCacheStorage}
     */
    private QueueableCacheStorage cacheStorage;

    /**
     * where to get a callback from. gives us the ability to use
     * customized {@link FilterawareRequestCallback}
     */
    private CallbackFactory callbackFactory;

    /**
     * the one and only constructor
     * @param cacheStorage
     * @param cf
     */
    public CachingDispatcherFilter(QueueableCacheStorage cacheStorage, CallbackFactory cf) {
        this.cacheStorage = cacheStorage;
        callbackFactory = cf;
    }

    protected CacheKey cacheKey(RequestBuilder builder) {
        if (RequestBuilder.GET.toString().equalsIgnoreCase(builder.getHTTPMethod())) {
            return new ComplexCacheKey(builder);
        }
        return null;
    }

    /**
     * main filter method for a dispatcherfilter.
     *
     * @return continue filtering or not
     */
    @Override
    public boolean filter(Method method, final RequestBuilder builder) {
        CacheKey cacheKey = cacheKey(builder);

        if (cacheKey != null) {
            final Response cachedResponse = cacheStorage.getResultOrReturnNull(cacheKey);
            if (cachedResponse != null) {
                //case 1: we got a result in cache => return it...
                if (LogConfiguration.loggingIsEnabled()) {
                    Logger.getLogger(Dispatcher.class.getName())
                        .info("already got a cached response for: " + builder.getHTTPMethod() + " " + builder.getUrl());
                }
                // onResponseReceived can be time consuming and can manipulate the DOM
                // deferring the command keeps the async behaviour of this method call
                Scheduler.get().scheduleDeferred(new ScheduledCommand() {

                    @Override
                    public void execute() {
                        builder.getCallback().onResponseReceived(null, cachedResponse);
                    }
                });
                return false;
            }
            RequestCallback callback = callbackFactory.createCallback(method);

            //case 2: => no cache in result => queue it....
            if (!cacheStorage.hasCallback(cacheKey)) {
                //case 2.1 => first callback => make a new one and execute...
                cacheStorage.addCallback(cacheKey, builder.getCallback());

                if (LogConfiguration.loggingIsEnabled()) {
                    Logger.getLogger(Dispatcher.class.getName())
                        .info("Sending *caching* http request: " + builder.getHTTPMethod() + " " + builder.getUrl());
                }

                // important part:
                builder.setCallback(callback);
                return true;
            }
            //case 2.2 => a callback already in progress => queue to get response when back
            if (LogConfiguration.loggingIsEnabled()) {
                Logger.getLogger(Dispatcher.class.getName())
                    .info("request in progress, queue callback: " + builder.getHTTPMethod() + " " + builder.getUrl());
            }
            cacheStorage.addCallback(cacheKey, callback);
            return false;
        }
        // non cachable case
        if (LogConfiguration.loggingIsEnabled()) {
            String content = builder.getRequestData();
            Logger.getLogger(Dispatcher.class.getName()).info(
                "Sending *non-caching* http request: " + builder.getHTTPMethod() + " " + builder.getUrl() +
                    " (Content: `" + content + "´)");
        }

        //            /*
        //             * add X-Request-Token to all non-caching calls (!= GET) if we have some
        //             */
        //            builder.setHeader("X-Testing", "Bude");

        builder.setCallback(callbackFactory.createCallback(method));
        return true;// continue filtering
    }
}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.rebind;

import static org.fusesource.restygwt.rebind.util.AnnotationUtils.getAnnotation;
import static org.fusesource.restygwt.rebind.util.AnnotationUtils.getClassAnnotation;
import static org.fusesource.restygwt.rebind.util.ClassSourceFileComposerFactoryImportUtil.addFuseSourceStaticImports;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.HasAnnotations;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JConstructor;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONNull;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.annotation.XmlTransient;

import org.fusesource.restygwt.client.Json;
import org.fusesource.restygwt.client.Json.Style;
import org.fusesource.restygwt.rebind.util.AnnotationUtils;

/**
 *
 * @author Hiram Chirino
 *
 *         Updates: added getter + setter support, enhanced generics support
 * @author Dave Finch
 *
 *         added polymorphic support
 * @author Charlie Mason
 *
 */

public class JsonEncoderDecoderClassCreator extends BaseSourceCreator {
    private static final String JSON_ENCODER_SUFFIX = "_Generated_JsonEncoderDecoder_";

    public static final String USE_JAVA_BEANS_SPEC_NAMING_CONVENTION_CONFIGURATION_PROPERTY_NAME =
        "restygwt.conventions.useJavaBeansSpecNaming";

    public String JSON_ENCODER_DECODER_CLASS = JsonEncoderDecoderInstanceLocator.JSON_ENCODER_DECODER_CLASS;
    protected static final String JSON_VALUE_CLASS = JSONValue.class.getName();
    private static final String JSON_OBJECT_CLASS = JSONObject.class.getName();
    private static final String JSON_ARRAY_CLASS = JSONArray.class.getName();
    private static final String JSON_NULL_CLASS = JSONNull.class.getName();
    protected static final String JSON_STRING_CLASS = JSONString.class.getName();

    protected EncoderDecoderLocator locator;

    protected boolean javaBeansNamingConventionEnabled;

    public JsonEncoderDecoderClassCreator(TreeLogger logger, GeneratorContext context, JClassType source) {
        super(logger, context, source, JSON_ENCODER_SUFFIX);

        // true, if the naming convention from JavaBeans API specification should be used
        javaBeansNamingConventionEnabled = getBooleanProperty(getLogger(), context.getPropertyOracle(),
            USE_JAVA_BEANS_SPEC_NAMING_CONVENTION_CONFIGURATION_PROPERTY_NAME, true);
    }

    @Override
    public void generate() throws UnableToCompleteException {
        JsonTypeInfo typeInfo = getClassAnnotation(source, JsonTypeInfo.class);
        boolean isLeaf = isLeaf(source);

        List<Subtype> possibleTypes = getPossibleTypes(typeInfo, isLeaf);
        Collections.sort(possibleTypes);

        JClassType sourceClazz = source.isClass() == null ? source.isInterface() : source.isClass();
        if (sourceClazz == null) {
            getLogger().log(ERROR, "Type is not a class");
            throw new UnableToCompleteException();
        }

        if (sourceClazz.isEnum() == null && sourceClazz.isAbstract()) {
            if (typeInfo == null) {
                getLogger().log(ERROR, "Abstract classes must be annotated with JsonTypeInfo");
                throw new UnableToCompleteException();
            }
        }
        Json jsonAnnotation = getAnnotation(source, Json.class);
        Style classStyle = jsonAnnotation != null ? jsonAnnotation.style() : Style.DEFAULT;
        String railsWrapperName =
                jsonAnnotation != null && !jsonAnnotation.name().isEmpty() ? jsonAnnotation.name() :
                sourceClazz.getName().toLowerCase();
        locator = EncoderDecoderLocatorFactory.getEncoderDecoderInstanceLocator(context, getLogger());

        generateSingleton(shortName);

        generateEncodeMethod(source, classStyle, typeInfo, railsWrapperName, possibleTypes, isLeaf, locator);

        generateDecodeMethod(source, classStyle, typeInfo, railsWrapperName, possibleTypes, isLeaf, locator);
    }

    @Override
    protected ClassSourceFileComposerFactory createComposerFactory() {
        ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(packageName, shortName);
        addFuseSourceStaticImports(composerFactory);
        composerFactory
            .setSuperclass(JSON_ENCODER_DECODER_CLASS + "<" + source.getParameterizedQualifiedSourceName() + ">");
        return composerFactory;
    }

    private List<Subtype> getPossibleTypes(JsonTypeInfo typeInfo, boolean isLeaf)
        throws UnableToCompleteException {
        if (typeInfo == null) {
            return Lists.newArrayList(new Subtype(null, source));
        }
        Collection<Type> subTypes = findJsonSubTypes(source);
        if (subTypes.isEmpty()) {
            JsonSubTypes foundAnnotation = getAnnotation(source, JsonSubTypes.class);
            if (foundAnnotation != null) {
                Type[] value = foundAnnotation.value();
                subTypes = Arrays.asList(value);
            }
        }
        PossibleTypesVisitor v = new PossibleTypesVisitor(context, source, isLeaf, getLogger(), subTypes);
        return v.visit(typeInfo.use());
    }

    /**
     * This method does NOT return the subtypes of the given class, but all the subtypes associated with the
     * {@link JsonSubTypes} annotation, even if this annotation is assigned to
     * a parent class or an interface.
     */
    private Collection<Type> findJsonSubTypes(JClassType clazz) {
        return findJsonSubTypes(clazz, new HashSet<JsonSubTypes.Type>());
    }

    private Collection<Type> findJsonSubTypes(JClassType clazz, Set<Type> types) {
        if (clazz == null) {
            return Collections.emptyList();
        }
        JsonSubTypes annotation = getClassAnnotation(clazz, JsonSubTypes.class);

        if (annotation == null) {
            return Collections.emptyList();
        }

        for (Type type : annotation.value()) {
            if (types.add(type)) {
                Class<?> subclazz = type.value();
                String newSubClassName = subclazz.getName().replaceAll("\\$", ".");
                JClassType subJClazz = context.getTypeOracle().findType(newSubClassName);
                findJsonSubTypes(subJClazz, types);
            }
        }

        return types;
    }

    protected void generateSingleton(String shortName) {
        p();
        p("public static final " + shortName + " INSTANCE = new " + shortName + "();");
        p();
    }

    private void generateEncodeMethod(JClassType classType, final Style classStyle, JsonTypeInfo typeInfo,
                                      String railsWrapperName, List<Subtype> possibleTypes, boolean isLeaf,
                                      final EncoderDecoderLocator locator) throws UnableToCompleteException {
        if (null != classType.isEnum()) {
            generateEnumEncodeMethod(classType, JSON_VALUE_CLASS);
            return;
        }

        p("public " + JSON_VALUE_CLASS + " encode(" + source.getParameterizedQualifiedSourceName() + " value) {").i(1);
        {
            p("if( value==null ) {").i(1);
            {
                p("return getNullType();");
            }
            i(-1).p("}");

            boolean returnWrapper = false; // if set, return rrc

            p(JSON_OBJECT_CLASS + " rc = new " + JSON_OBJECT_CLASS + "();");
            if (classStyle == Style.RAILS) {
                returnWrapper = true;
                p(JSON_OBJECT_CLASS + " rrc = new " + JSON_OBJECT_CLASS + "();");
                p("rrc.put(\"" + railsWrapperName + "\" , rc);");
            }

            for (Subtype possibleType : possibleTypes) {

                if (!possibleType.clazz.isAssignableTo(classType)) {
                    getLogger().log(DEBUG, "Only assignable classes are allowed: " +
                        possibleType.clazz.getParameterizedQualifiedSourceName() + " is not assignable to: " +
                        classType.getParameterizedQualifiedSourceName());
                    continue;
                }

                if (!isLeaf && possibleTypes.size() > 1) {
                    // Generate a decoder for each possible type
                    p("if(value.getClass().getName().equals(\"" + possibleType.clazz.getQualifiedBinaryName() + "\"))");
                    p("{");
                }

                if (possibleType.clazz.isEnum() != null) {
                    generateEnumEncodeMethodBody(possibleType, typeInfo);
                } else {

                    // Try to find a constructor that is annotated as creator
                    JConstructor creator = findCreator(possibleType.clazz);
                    List<JField> fields = getFields(possibleType.clazz);

                    List<JField> orderedFields = creator == null ? null : getOrderedFields(fields, creator);

                    if (typeInfo != null) {
                        switch (typeInfo.include()) {
                            case PROPERTY:
                                p("com.google.gwt.json.client.JSONValue className=STRING.encode(\"" + possibleType.tag + "\");");
                                p("if( className!=null ) { ").i(1);
                                p("rc.put(" + wrap(getTypeInfoPropertyValue(typeInfo)) + ", className);");
                                i(-1).p("}");
                                break;
                            case WRAPPER_OBJECT:
                                returnWrapper = true;
                                p(JSON_OBJECT_CLASS + " rrc = new " + JSON_OBJECT_CLASS + "();");
                                p("rrc.put(\"" + possibleType.tag + "\", rc);");
                                break;
                            case WRAPPER_ARRAY:
                                returnWrapper = true;
                                p(JSON_ARRAY_CLASS + " rrc = new " + JSON_ARRAY_CLASS + "();");
                                p("rrc.set(0, STRING.encode" +
                                    "(\"" + possibleType.tag + "\"));");
                                p("rrc.set(1, rc);");
                                break;
                            case EXISTING_PROPERTY:
                                getLogger().log(WARN, classType + " comes with not implemented type info 'as' " +
                                    JsonTypeInfo.As.EXISTING_PROPERTY);
                                // not implemented
                                break;
                            case EXTERNAL_PROPERTY:
                                getLogger().log(WARN, classType + " comes with not implemented type info 'as' " +
                                    JsonTypeInfo.As.EXTERNAL_PROPERTY);
                                // not implemented
                                break;
                            default:
                        }
                    }

                    p(possibleType.clazz.getParameterizedQualifiedSourceName() + " parseValue = (" +
                        possibleType.clazz.getParameterizedQualifiedSourceName() + ")value;");

                    for (final JField field : fields) {

                        final String getterName = getGetterName(possibleType.clazz, field);

                        boolean ignoreField = false;
                        if (getAnnotation(possibleType.clazz, JsonIgnoreProperties.class) != null) {
                            for (String s : getAnnotation(possibleType.clazz, JsonIgnoreProperties.class).value()) {
                                if (s.equals(field.getName())) {
                                    ignoreField = true;
                                    break;
                                }
                            }
                        }

                        // If can ignore some fields right off the back..
                        // if there is a creator encode only final fields with JsonProperty annotation
                        if (ignoreField || getterName == null && (field.isStatic() ||
                            (field.isFinal() && !(creator != null && orderedFields.contains(field))) ||
                            field.isTransient() || isIgnored(field))) {
                            continue;
                        }

                        branch("Processing field: " + field.getName(), new Branch<Void>() {
                            @Override
                            public Void execute() throws UnableToCompleteException {
                                // TODO: try to get the field with a setter or
                                // JSNI
                                if (getterName != null || field.isDefaultAccess() || field.isProtected() ||
                                    field.isPublic()) {

                                    Json jsonAnnotation = getAnnotation(field, Json.class);
                                    JsonProperty jsonPropertyAnnotation = getAnnotation(field, JsonProperty.class);

                                    String name = field.getName();
                                    String jsonName = name;

                                    if (jsonAnnotation != null && !jsonAnnotation.name().isEmpty()) {
                                        jsonName = jsonAnnotation.name();
                                    }
                                    if (jsonPropertyAnnotation != null && jsonPropertyAnnotation.value() != null &&
                                            !jsonPropertyAnnotation.value().isEmpty()) {
                                        jsonName = jsonPropertyAnnotation.value();
                                    }

                                    String fieldExpr = "parseValue." + name;
                                    if (getterName != null) {
                                        fieldExpr = "parseValue." + getterName + "()";
                                    }

                                    Style style = jsonAnnotation != null ? jsonAnnotation.style() : classStyle;
                                    String expression = locator.encodeExpression(field.getType(), fieldExpr, style);

                                    if (null != field.getType().isEnum()) {
                                        p("if(isNotNullAndCheckDefaults(" + fieldExpr + ", rc, " + wrap(jsonName) +
                                            ")) {").i(1);
                                    }

                                    p("isNotNullValuePut(" + expression + ", rc, " + wrap(jsonName) + ");");

                                    if (null != field.getType().isEnum()) {
                                        i(-1).p("}");
                                    }

                                } else {
                                    getLogger().log(DEBUG, "private field gets ignored: " +
                                        field.getEnclosingType().getQualifiedSourceName() + "." + field.getName());
                                }
                                return null;
                            }
                        });

                    }

                    if (returnWrapper) {
                        p("return rrc;");
                    } else {
                        p("return rc;");
                    }
                }
                if (!isLeaf && possibleTypes.size() > 1) {
                    p("}");
                }
            }

            if (!isLeaf && possibleTypes.size() > 1) {
                // Shouldn't get called
                p("return null;");
            }
        }
        i(-1).p("}");
        p();
    }

    private void generateEnumEncodeMethodBody(Subtype possibleType, JsonTypeInfo typeInfo) {
        p("if( value==null ) {").i(1);
        {
            p("return " + JSON_NULL_CLASS + ".getInstance();").i(-1);
        }
        p("}");
        p(JSON_OBJECT_CLASS + " rrc = new " + JSON_OBJECT_CLASS + "();");
        p(JSON_VALUE_CLASS + " className=STRING.encode(\"" +
            possibleType.tag + "\");");
        p("rrc.put(" + wrap(getTypeInfoPropertyValue(typeInfo)) + ", className);");
        p("rrc.put(\"name\", new " + JSON_STRING_CLASS + "(value." + getValueMethod(possibleType.clazz) + "()));");
        p("return rrc;");
    }

    private void generateEnumEncodeMethod(JClassType classType, String jsonValueClass) {
        p();
        p("public " + jsonValueClass + " encode(" + classType.getParameterizedQualifiedSourceName() + " value) {").i(1);
        {
            p("if( value==null ) {").i(1);
            {
                p("return " + JSON_NULL_CLASS + ".getInstance();").i(-1);
            }
            p("}");
            p("return new " + JSON_STRING_CLASS + "(value." + getValueMethod(classType) + "());");
            i(-1).p("}");
        }
        p();
    }

    protected String getValueMethod(JClassType classType) {
        String method = "name";
        for (JMethod jm : classType.isEnum().getMethods()) {
            if (jm.isAnnotationPresent(JsonValue.class)) {
                method = jm.getName();
                break;
            }
        }
        return method;
    }

    private void generateDecodeMethod(JClassType classType, final Style classStyle, JsonTypeInfo typeInfo,
                                      String railsWrapperName, List<Subtype> possibleTypes, boolean isLeaf,
                                      final EncoderDecoderLocator locator) throws UnableToCompleteException {
        if (null != classType.isEnum()) {
            generateEnumDecodeMethod(classType, JSON_VALUE_CLASS);
            return;
        }

        p("public " + source.getParameterizedQualifiedSourceName() + " decode(" + JSON_VALUE_CLASS + " value) {").i(1);
        {
            p("if( value == null || value.isNull()!=null ) {").i(1);
            {
                p("return null;").i(-1);
            }
            p("}");
            if (classStyle == Style.RAILS) {
                p(JSON_OBJECT_CLASS + " object = toObjectFromWrapper(value, \"" + railsWrapperName + "\");");
            } else if (typeInfo != null && typeInfo.include() == As.WRAPPER_ARRAY) {
                p(JSON_ARRAY_CLASS + " array = (" + JSON_ARRAY_CLASS + ")value;");
                if (!isLeaf) {
                    p("String sourceName = STRING.decode" +
                        "(array.get(0));");
                }
                p(JSON_OBJECT_CLASS + " object = toObject(array.get(1));");
            } else {
                p(JSON_OBJECT_CLASS + " object = toObject(value);");
            }

            if (!isLeaf && typeInfo != null && typeInfo.include() == As.PROPERTY) {
                p("String sourceName = STRING.decode(object" +
                    ".get(" + wrap(getTypeInfoPropertyValue(typeInfo)) + "));");
            }

            for (Subtype possibleType : possibleTypes) {

                if (!possibleType.clazz.isAssignableTo(classType)) {
                    getLogger().log(DEBUG, "Only assignable classes are allowed: " +
                        possibleType.clazz.getParameterizedQualifiedSourceName() + " is not assignable to: " +
                        classType.getParameterizedQualifiedSourceName());
                    continue;
                }

                if (typeInfo != null) {
                    if (typeInfo.include() == As.WRAPPER_OBJECT) {
                        if (!isLeaf) {
                            p("if(object.containsKey(\"" + possibleType.tag + "\"))");
                            p("{");
                        }
                        p("object = toObjectFromWrapper(value, \"" + possibleType.tag + "\");");
                    } else if (!isLeaf) {
                        if (classType.equals(possibleType.clazz)) {
                            p("if(sourceName == null || sourceName.equals(\"" + possibleType.tag + "\"))");
                            p("{");
                        } else {
                            p("if(\"" + possibleType.tag + "\".equals(sourceName))");
                            p("{");
                        }
                    }
                }

                if (possibleType.clazz.isEnum() != null) {
                    generateEnumDecodeMethodBody(possibleType.clazz);
                } else {
                    // Try to find a constuctor that is annotated as creator
                    JConstructor creator = findCreator(possibleType.clazz);

                    List<JField> orderedFields = null;
                    if (creator != null) {
                        p("// We found a creator so we use the annotated constructor");
                        p("" + possibleType.clazz.getParameterizedQualifiedSourceName() + " rc = new " +
                            possibleType.clazz.getParameterizedQualifiedSourceName() + "(");
                        i(1).p("// The arguments are placed in the order they appear within the annotated constructor")
                            .i(-1);
                        orderedFields = getOrderedFields(getFields(possibleType.clazz), creator);
                        final JField lastField = orderedFields.get(orderedFields.size() - 1);
                        for (final JField field : orderedFields) {
                            branch("Processing field: " + field.getName(), new Branch<Void>() {
                                @Override
                                public Void execute() throws UnableToCompleteException {
                                    Json jsonAnnotation = getAnnotation(field, Json.class);
                                    Style style = jsonAnnotation != null ? jsonAnnotation.style() : classStyle;
                                    String jsonName = field.getName();
                                    if (jsonAnnotation != null && !jsonAnnotation.name().isEmpty()) {
                                        jsonName = jsonAnnotation.name();
                                    }
                                    String objectGetter = "object.get(" + wrap(jsonName) + ")";
                                    String expression = locator.decodeExpression(field.getType(), objectGetter, style);

                                    String defaultValue = getDefaultValue(field);
                                    i(1).p("" + (objectGetter + " == null || " + objectGetter + " instanceof " +
                                        JSON_NULL_CLASS + " ? " + defaultValue + " : " + expression +
                                        ((field != lastField) ? ", " : ""))).i(-1);

                                    return null;
                                }
                            });
                        }
                        p(");");
                    }

                    if (orderedFields == null) {
                        p("" + possibleType.clazz.getParameterizedQualifiedSourceName() + " rc = new " +
                            possibleType.clazz.getParameterizedQualifiedSourceName() + "();");
                    }

                    for (final JField field : getFields(possibleType.clazz)) {

                        boolean ignoreField = false;
                        if (getAnnotation(possibleType.clazz, JsonIgnoreProperties.class) != null) {
                            for (String s : getAnnotation(possibleType.clazz, JsonIgnoreProperties.class).value()) {
                                if (s.equals(field.getName())) {
                                    ignoreField = true;
                                    break;
                                }
                            }
                        }
                        if (ignoreField) {
                            continue;
                        }

                        if (orderedFields != null && orderedFields.contains(field)) {
                            continue;
                        }

                        final String setterName = getSetterName(field);

                        // If can ignore some fields right off the back..
                        if (setterName == null && (field.isStatic() || field.isFinal() || field.isTransient()) ||
                            isIgnored(field)) {
                            continue;
                        }

                        branch("Processing field: " + field.getName(), new Branch<Void>() {
                            @Override
                            public Void execute() throws UnableToCompleteException {

                                // TODO: try to set the field with a setter
                                // or JSNI
                                if (setterName != null || field.isDefaultAccess() || field.isProtected() ||
                                    field.isPublic()) {

                                    Json jsonAnnotation = getAnnotation(field, Json.class);
                                    Style style = jsonAnnotation != null ? jsonAnnotation.style() : classStyle;
                                    JsonProperty jsonPropertyAnnotation = getAnnotation(field, JsonProperty.class);

                                    String name = field.getName();
                                    String jsonName = name;

                                    if (jsonAnnotation != null && !jsonAnnotation.name().isEmpty()) {
                                        jsonName = jsonAnnotation.name();
                                    }
                                    if (jsonPropertyAnnotation != null && jsonPropertyAnnotation.value() != null &&
                                            !jsonPropertyAnnotation.value().isEmpty()) {
                                        jsonName = jsonPropertyAnnotation.value();
                                    }

                                    String objectGetter = "object.get(" + wrap(jsonName) + ")";
                                    String expression = locator.decodeExpression(field.getType(), objectGetter, style);

                                    boolean isShort = field.getType().isPrimitive() == JPrimitiveType.SHORT;
                                    String defaultValue = getDefaultValue(field);

                                    String methodName = isShort ? "getValueToSetForShort" : "getValueToSet";

                                    if (setterName != null) {
                                        p("rc." + setterName + "(" + methodName + "(" + expression + ", " +
                                            defaultValue + "));");
                                    } else {
                                        p("rc." + name + "= " + methodName + "(" + expression + "," + defaultValue +
                                            ");");
                                    }

                                } else {
                                    getLogger().log(DEBUG, "private field gets ignored: " +
                                        field.getEnclosingType().getQualifiedSourceName() + "." + field.getName());
                                }
                                return null;
                            }
                        });
                    }

                    p("return rc;");
                }
                if (typeInfo != null && !isLeaf) {
                    p("}");
                }
            }

            if (typeInfo != null && !isLeaf) {
                p("return null;");
            }
            i(-1).p("}");
            p();
        }
    }

    private void generateEnumDecodeMethodBody(JClassType classType) {
        p(JSON_VALUE_CLASS + " str = object.get(\"name\");");
        p("if( null == str || str.isString() == null ) {").i(1);
        {
            p("throw new DecodingException(\"Expected a string field called 'name' for enum; not found\");").i(-1);
        }
        p("}");
        decodeEnum(classType, "str.isString().stringValue()");
    }

    private String getDefaultValue(JField field) {
        return field.getType().isPrimitive() == null ? "null" :
            field.getType().isPrimitive().getUninitializedFieldExpression() + "";
    }

    protected void generateEnumDecodeMethod(JClassType classType, String jsonValueClass) {
        p();
        p("public " + classType.getName() + " decode(" + jsonValueClass + " value) {").i(1);
        {
            p("if( value == null || value.isNull()!=null ) {").i(1);
            {
                p("return null;").i(-1);
            }
            p("}");
            p(JSON_STRING_CLASS + " str = value.isString();");
            p("if( null == str ) {").i(1);
            {
                p("throw new DecodingException(\"Expected a json string (for enum), but was given: \"+value);").i(-1);
            }
            p("}");

            String value = "str.stringValue()";
            decodeEnum(classType, value);
        }
        p("}");
        p();
    }

    protected void decodeEnum(JClassType classType, String value) {
        String className = classType.getParameterizedQualifiedSourceName();
        String method = getValueMethod(classType);
        if (method == null) {
            p("return Enum.valueOf(" + className + ".class, " + value + ");").i(-1);
        } else {
            p("for(" + className + " v: " + className + ".values()) {").i(1);
            {
                p("if(v." + method + "().equals(" + value + ")) {").i(1);
                {
                    p("return v;").i(-1);
                }
                p("}").i(-1);
            }
            p("}");
            p("throw new DecodingException(\"can not find enum for given value: \"+" + value + ");").i(-1);
        }
    }

    public static void clearRestyResolverClassMap() {
        sTypeIdResolverMap = null;
    }

    public static Map<Class<?>, RestyJsonTypeIdResolver> getRestyResolverClassMap(GeneratorContext context,
                                                                                TreeLogger logger)
        throws UnableToCompleteException {
        if (sTypeIdResolverMap == null) {
            try {
                Map<Class<?>, RestyJsonTypeIdResolver> map = Maps.newHashMap();
                List<String> values =
                    context.getPropertyOracle().getConfigurationProperty("org.fusesource.restygwt.jsontypeidresolver")
                        .getValues();
                for (String value : values) {
                    try {
                        Class<?> clazz = Class.forName(value);
                        RestyJsonTypeIdResolver resolver = (RestyJsonTypeIdResolver) clazz.newInstance();
                        map.put(resolver.getTypeIdResolverClass(), resolver);
                    } catch (Exception e) {
                        logger.log(WARN, "Could not access class: " + values.get(0), e);
                    }
                }
                sTypeIdResolverMap = map;
            } catch (BadPropertyValueException e) {
                logger.log(ERROR, "Could not acccess property: RestyJsonTypeIdResolver", e);
                throw new UnableToCompleteException();
            }
        }
        return sTypeIdResolverMap;
    }

    private List<JField> getOrderedFields(List<JField> fields, JConstructor creator) throws UnableToCompleteException {
        List<JField> orderedFields = new ArrayList<JField>();
        for (JParameter param : creator.getParameters()) {
            JsonProperty prop = getAnnotation(param, JsonProperty.class);
            if (prop != null) {
                for (JField field : fields) {
                    if (field.getName().equals(prop.value())) {
                        orderedFields.add(field);
                    }
                }
            } else {
                getLogger().log(ERROR,
                    "a constructor annotated with @JsonCreator requires that all paramaters are annotated with " +
                        "@JsonProperty.");
                throw new UnableToCompleteException();
            }
        }

        return orderedFields;
    }

    private JConstructor findCreator(JClassType sourceClazz) {
        for (JConstructor constructor : sourceClazz.getConstructors()) {
            if (getAnnotation(constructor, JsonCreator.class) != null) {
                return constructor;
            }
        }

        return null;
    }

    /**
     *
     * @param field
     * @return the name for the setter for the specified field or null if a
     *         setter can't be found.
     */
    private String getSetterName(JField field) {
        String fieldName = "set" + getMiddleNameForPrefixingAsAccessorMutator(field.getName());
        JClassType type = field.getEnclosingType();
        if (exists(type, field, fieldName, true)) {
            return fieldName;
        }
        return null;
    }

    /**
     *
     * @param field
     * @return the name for the getter for the specified field or null if a
     *         getter can't be found.
     */
    private String getGetterName(JClassType type, JField field) {
        String methodBaseName = getMiddleNameForPrefixingAsAccessorMutator(field.getName());
        String fieldName;
        JType booleanType = null;
        try {
            booleanType = find(Boolean.class, getLogger(), context);
        } catch (UnableToCompleteException e) {
            // do nothing
        }
        if (field.getType().equals(JPrimitiveType.BOOLEAN) || field.getType().equals(booleanType)) {
            if (field instanceof DummyJField) {
                return ((DummyJField) field).getGetterMethod().getName();
            }
            fieldName = "is" + methodBaseName;
            if (exists(type, field, fieldName, false)) {
                return fieldName;
            }
            fieldName = "has" + methodBaseName;
            if (exists(type, field, fieldName, false)) {
                return fieldName;
            }
        }
        fieldName = "get" + methodBaseName;
        if (exists(type, field, fieldName, false)) {
            return fieldName;
        }
        return null;
    }

    private String getMiddleNameForPrefixingAsAccessorMutator(String fieldName) {
        if (javaBeansNamingConventionEnabled) {
            return getMiddleNameForPrefixingAsAccessorMutatorJavaBeansSpecCompliance(fieldName);
        }
        return upperCaseFirstChar(fieldName);
    }

    /**
     * Get the middle part of the method name in compliance with the naming convention in the JavaBeans API
     * specification.
     *
     * @param fieldName
     * @return
     */
    static String getMiddleNameForPrefixingAsAccessorMutatorJavaBeansSpecCompliance(String fieldName) {
        if (fieldName.length() > 1 && Character.isUpperCase(fieldName.charAt(1))) {
            return fieldName;
        }
        return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

    private String upperCaseFirstChar(String in) {
        if (in.length() == 1) {
            return in.toUpperCase();
        }
        return in.substring(0, 1).toUpperCase() + in.substring(1);
    }

    /**
     * Checks if hasAnnotations should be ignored based on JsonIgnore and XmlTransient
     *
     * @param hasAnnotations
     * @return true if hasAnnotations should be ignored
     * @see #isJsonIgnored(HasAnnotations)
     * @see #isXmlTransient(HasAnnotations)
     */
    private boolean isIgnored(HasAnnotations hasAnnotations) {
        return isJsonIgnored(hasAnnotations) || isXmlTransient(hasAnnotations);
    }

    /**
     * @param hasAnnotations
     * @return true if hasAnnotations is annotated with @JsonIgnore and its value is true
     * @see AnnotationUtils#getAnnotation(HasAnnotations, Class)
     */
    private boolean isJsonIgnored(HasAnnotations hasAnnotations) {
        return isJsonIgnored(getAnnotation(hasAnnotations, JsonIgnore.class));
    }

    /**
     * @param jsonIgnore
     * @return true if jsonIgnore.value() is true
     */
    private boolean isJsonIgnored(JsonIgnore jsonIgnore) {
        return jsonIgnore != null && jsonIgnore.value();
    }

    /**
     * @param hasAnnotations
     * @return true of hasAnnotations is annotated with XmlTransient
     * @see AnnotationUtils#getAnnotation(HasAnnotations, Class)
     */
    private boolean isXmlTransient(HasAnnotations hasAnnotations) {
        return getAnnotation(hasAnnotations, XmlTransient.class) != null;
    }

    /**
     * checks whether a getter or setter exists on the specified type or any of
     * its super classes excluding Object. respects JsonIgnore accordingly.
     *
     * @param type
     * @param field
     * @param fieldName
     * @param isSetter
     * @return
     */
    private boolean exists(JClassType type, JField field, String fieldName, boolean isSetter) {
        if (field instanceof DummyJField) {
            return true;
        }

        JType[] args;
        if (isSetter) {
            args = new JType[] { field.getType() };
        } else {
            args = new JType[] {};
        }
        JMethod m = type.findMethod(fieldName, args);
        if (null != m) {
            if (isIgnored(m)) {
                return false;
            }
            if (isSetter) {
                return true;
            }
            JClassType returnType = m.getReturnType().isClassOrInterface();
            JClassType fieldType = field.getType().isClassOrInterface();
            if (returnType == null || fieldType == null) {
                // at least one is a primitive type
                return m.getReturnType().equals(field.getType());
            }
            // both are non-primitives
            return returnType.isAssignableFrom(fieldType);
        }
        try {
            JType objectType = find(Object.class, getLogger(), context);
            JClassType superType = type.getSuperclass();
            if (!objectType.equals(superType)) {
                return exists(superType, field, fieldName, isSetter);
            }
        } catch (UnableToCompleteException e) {
            // do nothing
        }
        return false;
    }

    /**
     * Get {@link JsonProperty} from getter or setter. Annotation from setter is preferred to getter.
     *
     * @param getter
     * @param setter
     * @return
     */
    private JsonProperty getJsonPropertyFromGetterSetter(JMethod getter, JMethod setter) {
        JsonProperty setterProp = getAnnotation(setter, JsonProperty.class);
        return (null != setterProp) ? setterProp : getAnnotation(getter, JsonProperty.class);
    }

    /**
     * Inspects the supplied type and all super classes up to but excluding
     * Object and returns a list of all fields found in these classes.
     *
     * @param type
     * @return
     */
    private List<JField> getFields(JClassType type) {
        List<JField> allFields = getFields(new ArrayList<JField>(), type);
        Map<String, JMethod> getters = new HashMap<String, JMethod>();
        Map<String, JMethod> setters = new HashMap<String, JMethod>();

        JType booleanType = null;
        try {
            booleanType = find(Boolean.class, getLogger(), context);
        } catch (UnableToCompleteException e) {
            // do nothing
        }
        for (JMethod m : type.getInheritableMethods()) {
            if (m.getName().startsWith("set") && m.getParameterTypes().length == 1 &&
                m.getReturnType() == JPrimitiveType.VOID && !isIgnored(m)) {
                setters.put(m.getName().substring("set".length()), m);
            } else if (m.getName().startsWith("get") && m.getParameterTypes().length == 0 &&
                m.getReturnType() != JPrimitiveType.VOID && !isIgnored(m)) {
                getters.put(m.getName().substring("get".length()), m);
            } else if (m.getName().startsWith("is") && m.getParameterTypes().length == 0 &&
                (m.getReturnType() == JPrimitiveType.BOOLEAN || m.getReturnType().equals(booleanType)) &&
                !isIgnored(m)) {
                getters.put(m.getName().substring("is".length()), m);
            } else if (m.getName().startsWith("has") && m.getParameterTypes().length == 0 &&
                (m.getReturnType() == JPrimitiveType.BOOLEAN || m.getReturnType().equals(booleanType)) &&
                !isIgnored(m)) {
                getters.put(m.getName().substring("has".length()), m);
            }
        }
        for (Map.Entry<String, JMethod> entry : getters.entrySet()) {
            JMethod getter = entry.getValue();
            JMethod setter = setters.get(entry.getKey());

            if (null != setter && setter.getParameterTypes()[0].equals(getter.getReturnType())) {
                String name = entry.getKey().substring(0, 1).toLowerCase() + entry.getKey().substring(1);
                JField f = null;
                for (JField field : allFields) {
                    if (field.getName().equals(name)) {
                        f = field;
                        break;
                    }
                }

                if (f != null && isJsonIgnored(f)) {
                    continue;
                }

                JsonProperty propName = getJsonPropertyFromGetterSetter(getter, setter);

                // if have a field and an annotation from the getter/setter then use that annotation
                if (f != null) {
                    if (propName != null && !f.getName().equals(propName.value())) {
                        allFields.remove(f);
                        DummyJField dummy = new DummyJField(name, getter.getReturnType(), getter);
                        dummy.setAnnotation(propName);
                        allFields.add(dummy);
                    }
                } else {
                    DummyJField dummy = new DummyJField(name, getter.getReturnType(), getter);
                    if (getter.isAnnotationPresent(JsonProperty.class)) {
                        dummy.setAnnotation(getAnnotation(getter, JsonProperty.class));
                    }
                    allFields.add(dummy);
                }
            }
        }

        // remove fields annotated with JsonIgnore
        for (Iterator<JField> iter = allFields.iterator(); iter.hasNext(); ) {
            JField field = iter.next();
            if (isJsonIgnored(field)) {
                iter.remove();
            }
        }

        return allFields;
    }

    /**
     * Returns a list of all fields (non {@code transient} and not annotated with {@link XmlTransient}) in the
     * supplied type and all super classes.
     *
     * @param allFields
     * @param type
     * @return
     */
    private List<JField> getFields(List<JField> allFields, JClassType type) {
        JField[] fields = type.getFields();
        for (JField field : fields) {
            if (!field.isTransient() && !isXmlTransient(field)) {
                allFields.add(field);
            }
        }
        try {
            JType objectType = find(Object.class, getLogger(), context);
            if (!objectType.equals(type)) {
                JClassType superType = type.getSuperclass();
                return getFields(allFields, superType);
            }
        } catch (UnableToCompleteException e) {
            // do nothing
        }

        return allFields;
    }

    public static String getTypeInfoPropertyValue(JsonTypeInfo typeInfo) {
        if (typeInfo.include() == JsonTypeInfo.As.PROPERTY) {
            if (typeInfo.property() == null || "".equals(typeInfo.property())) {
                return typeInfo.use().getDefaultPropertyName();
            }
        }

        return typeInfo.property();
    }

    public static boolean isLeaf(JClassType source) {
        return !(source.getSubtypes() != null && source.getSubtypes().length > 0);
    }

    public static class Subtype implements Comparable<Subtype> {
        final String tag;
        final JClassType clazz;

        public Subtype(String tag, JClassType clazz) {
            this.tag = tag;
            this.clazz = clazz;
        }

        @Override
        public int compareTo(Subtype o) {
            return tag.compareTo(o.tag);
        }
    }

    private static Map<Class<?>, RestyJsonTypeIdResolver> sTypeIdResolverMap = null;
}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.examples.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;

import org.fusesource.restygwt.client.Method;
import org.fusesource.restygwt.client.OverlayCallback;
import org.fusesource.restygwt.client.REST;
import org.fusesource.restygwt.client.Resource;
import org.fusesource.restygwt.client.RestServiceProxy;

/**
 * Entry point classes define onModuleLoad().
 *
 * @author Hiram Chirino
 */
public class UI implements EntryPoint {

    /**
     * This is the entry point method.
     */
    @Override
    public void onModuleLoad() {
        Button button = new Button("Get Greeting");
        button.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                getGreeting();
            }
        });
        RootPanel.get().add(button);
        RootPanel.get().add(new Label("Name:"));
        final TextBox nameInput = new TextBox();
        RootPanel.get().add(nameInput);
        Button customButton = new Button("Get Custom Greeting");
        customButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                getCustomGreeting(nameInput.getValue());
            }
        });
        RootPanel.get().add(customButton);
    }

    private void getGreeting() {
        GreetingService service = GWT.create(GreetingService.class);
        Resource resource = new Resource(GWT.getModuleBaseURL() + "greeting-service");
        ((RestServiceProxy) service).setResource(resource);

        REST.withCallback(new OverlayCallback<Greeting>() {
            @Override
            public void onSuccess(Method method, Greeting greeting) {
                RootPanel.get().add(new Label("server said: " + greeting.getGreeting()));
            }

            @Override
            public void onFailure(Method method, Throwable exception) {
                Window.alert("Error: " + exception);
            }
        }).call(service).getGreeting();
    }

    private void getCustomGreeting(String name) {
        GreetingService service = GWT.create(GreetingService.class);
        Resource resource = new Resource(GWT.getModuleBaseURL() + "greeting-service");
        ((RestServiceProxy) service).setResource(resource);
        NameObject arg = (NameObject) JavaScriptObject.createObject();
        arg.setName(name);

        REST.withCallback(new OverlayCallback<Greeting>() {
            @Override
            public void onSuccess(Method method, Greeting greeting) {
                RootPanel.get().add(new Label("server said: " + greeting.getGreeting()));
            }

            @Override
            public void onFailure(Method method, Throwable exception) {
                Window.alert("Error: " + exception);
            }
        }).call(service).getCustomGreeting(arg);
    }
}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.rebind;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;

import org.fusesource.restygwt.client.Json.Style;

/**
 *
 * @author Hiram Chirino
 */
public class GwtJacksonEncoderDecoderInstanceLocator extends JsonEncoderDecoderInstanceLocator {

    public GwtJacksonEncoderDecoderInstanceLocator(GeneratorContext context, TreeLogger logger)
        throws UnableToCompleteException {
        super(context, logger);
    }

    /* (non-Javadoc)
    * @see org.fusesource.restygwt.rebind.EncoderDecoderLocator#encodeExpression(com.google.gwt.core.ext.typeinfo
    * .JType, java.lang.String, org.fusesource.restygwt.client.Json.Style)
    */
    @Override
    public String encodeExpression(JType type, String expression, Style style) throws UnableToCompleteException {
        return encodeDecodeExpression(type, expression, "encode");
    }

    /* (non-Javadoc)
    * @see org.fusesource.restygwt.rebind.EncoderDecoderLocator#decodeExpression(com.google.gwt.core.ext.typeinfo
    * .JType, java.lang.String, org.fusesource.restygwt.client.Json.Style)
    */
    @Override
    public String decodeExpression(JType type, String expression, Style style) throws UnableToCompleteException {
        return encodeDecodeExpression(type, expression, "decode");
    }

    private String getEncoderDecoder(JType type, TreeLogger logger) throws UnableToCompleteException {
        String rc = builtInEncoderDecoders.get(type);
        if (rc == null) {
            JClassType ct = type.isClass() == null ? type.isInterface() : type.isClass();
            GwtJacksonEncoderDecoderClassCreator generator =
                new GwtJacksonEncoderDecoderClassCreator(logger, context, ct);
            return generator.create() + ".INSTANCE";
        }
        return rc;
    }

    private String encodeDecodeExpression(JType type, String expression, String encoderMethod)
        throws UnableToCompleteException {

        String encoderDecoder = getEncoderDecoder(type, logger);
        if (encoderDecoder != null) {
            return encoderDecoder + "." + encoderMethod + "(" + expression + ")";
        }
        error("Do not know how to encode/decode " + type);
        return null;
    }

}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.rebind;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;

import java.util.ArrayList;
import java.util.List;

/**
 * compile-time defaults
 *
 * with this class it will be possible to take part of the generation process of restserviceimpl
 * classes. by default there are no additional resolvers registered, you can add some if wanted.
 *
 * @see
 * <a href='http://code.google.com/p/google-web-toolkit/wiki/MultiValuedConfigProperties'>MultiValuedConfigProperties
 * </a>
 * @author abalke
 *
 */
public class BindingDefaults {
    /**
     * additional AnnotationResolvers, added by {@link #addAnnotationResolver(AnnotationResolver)}
     */
    private static final List<AnnotationResolver> annotationResolvers = new ArrayList<AnnotationResolver>();

    /**
     * to avoid returning different Sets of AnnotationResolvers by false
     * usage, the return of the first call to {@link #getAnnotationResolvers()}
     * is cached here.
     *
     * if there will be a call to {@link #addAnnotationResolver(AnnotationResolver)} after
     * that first call, we will throw a runtime exception to avoid nasty bugs.
     */
    private static List<AnnotationResolver> _annotationResolversRequested = null;

    /**
     * access all annotationresolvers that are registered
     *
     * @return a copy of all AnnotationResolvers
     */
    public static List<AnnotationResolver> getAnnotationResolvers(GeneratorContext context,
                                                                  TreeLogger logger) {

        // do this only the first time call
        if (null == _annotationResolversRequested) {
            // call additional AnnotationResolvers if there are some configured
            try {
                for (String className : context.getPropertyOracle()
                    .getConfigurationProperty("org.fusesource.restygwt.annotationresolver").getValues()) {
                    logger.log(TreeLogger.INFO, "classname to resolve: " + className);
                    Class<?> clazz;

                    try {
                        clazz = Class.forName(className);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException("could not resolve class " + className + " " + e.getMessage());
                    }

                    if (null != clazz) {
                        try {
                            logger.log(TreeLogger.INFO, "add annotationresolver: " + clazz.getName());
                            addAnnotationResolver((AnnotationResolver) clazz.newInstance());
                        } catch (InstantiationException e) {
                            throw new RuntimeException("could not instanciate class " + className + " " + e.getMessage());
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException("could not access class " + className + " " + e.getMessage());
                        }
                    } else {
                        throw new RuntimeException("could not create instance for classname " + className);
                    }
                }
            } catch (BadPropertyValueException ignored) {
                /*
                 *  ignored since there is no
                 *
                 *  <set-configuration-property name="org.fusesource.restygwt.annotationresolver"
                 *          value="org.fusesource.restygwt.rebind.ModelChangeAnnotationResolver"/>
                 */
                logger.log(TreeLogger.DEBUG, "no additional annotationresolvers found");
            }
        }

        // return a copy
        List<AnnotationResolver> ret = new ArrayList<AnnotationResolver>();

        ret.addAll(annotationResolvers);
        return _annotationResolversRequested = ret;
    }

    /**
     * access all annotationresolvers that are registered
     */
    public static void addAnnotationResolver(AnnotationResolver ar) {
        if (_annotationResolversRequested != null) {
            throw new RuntimeException("Sorry, you cannot add more AnnotationResolver instances after the first time " +
                "`BindingDefaults#getAnnotationResolvers´ has been called. please check your runtime logic.");
        }
        annotationResolvers.add(ar);
    }
}


/**
 * Copyright (C) 2009-2015 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.logging.client.LogConfiguration;
import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.XMLParser;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.fusesource.restygwt.rebind.AnnotationResolver;

/**
 *
 * @author Hiram Chirino
 */
public class Method {

    /**
     * GWT hides the full spectrum of methods because safari has a bug:
     * http://bugs.webkit.org/show_bug.cgi?id=3812
     *
     * We extend assume the server side will also check the
     * X-HTTP-Method-Override header.
     *
     * TODO: add an option to support using this approach to bypass restrictive
     * firewalls even if the browser does support the setting all the method
     * types.
     *
     * @author chirino
     */
    private static class MethodRequestBuilder extends RequestBuilder {
        public MethodRequestBuilder(String method, String url) {

            super(method, url);
            //without null value being explicitly set gwt would generate "undefined" as a default value,
            //so if request does not have a body, Internet Explorer would send string "undefined" in the body of
            // POST, PUT and DELETE requests,
            //which may cause the request to fall on server with "No operation matching request path"
            setRequestData(null);
            if (Defaults.isAddXHttpMethodOverrideHeader()) {
                setHeader("X-HTTP-Method-Override", method);
            }
        }
    }

    public RequestBuilder builder;

    final Set<Integer> expectedStatuses;

    {
        expectedStatuses = new HashSet<Integer>();
        expectedStatuses.add(200);
        expectedStatuses.add(201);
        expectedStatuses.add(204);
        // This is needed for MSIE mangling with status 204 to become 1223
        expectedStatuses.add(1223);
    }

    boolean anyStatus;

    Request request;
    Response response;
    Dispatcher dispatcher;

    /**
     * additional data which can be set per instance, e.g. from a {@link AnnotationResolver}
     */
    private final Map<String, String> data = new HashMap<String, String>();

    private Logger logger;

    protected Method() {
    }

    public Method(Resource resource, String method) {
        builder = new MethodRequestBuilder(method, resource.getUri());
    }

    public Method user(String user) {
        builder.setUser(user);
        return this;
    }

    public Method password(String password) {
        builder.setPassword(password);
        return this;
    }

    public Method header(String header, String value) {
        builder.setHeader(header, value);
        return this;
    }

    public Method headers(Map<String, String> headers) {
        if (headers != null) {
            for (Entry<String, String> entry : headers.entrySet()) {
                builder.setHeader(entry.getKey(), entry.getValue());
            }
        }
        return this;
    }

    private void doSetTimeout() {
        // Use default timeout only if it was not already set through the @Options(timeout =) annotation.
        // See https://github.com/resty-gwt/resty-gwt/issues/206
        if (builder.getTimeoutMillis() == 0 && Defaults.getRequestTimeout() > -1) {
            builder.setTimeoutMillis(Defaults.getRequestTimeout());
        }
    }

    public Method text(String data) {
        defaultContentType(Resource.CONTENT_TYPE_TEXT);
        builder.setRequestData(data);
        return this;
    }

    public Method json(JSONValue data) {
        defaultContentType(Resource.CONTENT_TYPE_JSON);
        builder.setRequestData(data.toString());

        return this;
    }

    public Method xml(Document data) {
        defaultContentType(Resource.CONTENT_TYPE_XML);
        builder.setRequestData(data.toString());
        return this;
    }

    public Method form(String encodedFormData) {
        defaultContentType(Resource.CONTENT_TYPE_FORM);
        builder.setRequestData(encodedFormData);
        return this;
    }

    public Method timeout(int timeout) {
        builder.setTimeoutMillis(timeout);
        return this;
    }

    /**
     * sets the expected response status code.  If the response status code does not match
     * any of the values specified then the request is considered to have failed.  Defaults to accepting
     * 200,201,204. If set to -1 then any status code is considered a success.
     */
    public Method expect(int... statuses) {
        if (statuses.length == 1 && statuses[0] < 0) {
            anyStatus = true;
        } else {
            anyStatus = false;
            expectedStatuses.clear();
            for (int status : statuses) {
                expectedStatuses.add(status);
            }
        }
        return this;
    }

    /**
     * Local file-system (file://) does not return any status codes.
     * Therefore - if we read from the file-system we accept all codes.
     *
     * This is for instance relevant when developing a PhoneGap application with
     * restyGwt.
     */
    public boolean isExpected(int status) {

        String baseUrl = GWT.getHostPageBaseURL();
        String requestUrl = builder.getUrl();

        if (FileSystemHelper.isRequestGoingToFileSystem(baseUrl, requestUrl)) {
            return true;
        } else if (anyStatus) {
            return true;
        } else {
            return expectedStatuses.contains(status);
        }
    }

    public Object send(RequestCallback callback) throws RequestException {
        doSetTimeout();
        builder.setCallback(callback);
        // lazily load dispatcher from defaults, if one is not set yet.
        Dispatcher localDispatcher = dispatcher == null ? Defaults.getDispatcher() : dispatcher;
        return localDispatcher.send(this, builder);
    }

    private Logger getLogger() {
        if (GWT.isClient() && LogConfiguration.loggingIsEnabled() && logger == null) {
            logger = Logger.getLogger(Method.class.getName());
        }
        return logger;
    }

    public Object send(TextCallback callback) {
        return send((MethodCallback<String>) callback);
    }

    public Object send(MethodCallback<String> callback) {
        defaultAcceptType(Resource.CONTENT_TYPE_TEXT);
        try {
            return send(new AbstractRequestCallback<String>(this, callback) {
                @Override
                protected String parseResult() throws Exception {
                    return response.getText();
                }
            });
        } catch (Throwable e) {
            if (getLogger() != null) {
                getLogger()
                    .log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
            }
            callback.onFailure(this, e);
            return null;
        }
    }

    public Object send(JsonCallback callback) {
        defaultAcceptType(Resource.CONTENT_TYPE_JSON);

        try {
            return send(new AbstractRequestCallback<JSONValue>(this, callback) {
                @Override
                protected JSONValue parseResult() throws Exception {
                    try {
                        return JSONParser.parseStrict(response.getText());
                    } catch (Throwable e) {
                        throw new ResponseFormatException("Response was NOT a valid JSON document", e);
                    }
                }
            });
        } catch (Throwable e) {
            if (getLogger() != null) {
                getLogger()
                    .log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
            }
            callback.onFailure(this, e);
            return null;
        }
    }

    public Object send(XmlCallback callback) {
        defaultAcceptType(Resource.CONTENT_TYPE_XML);
        try {
            return send(new AbstractRequestCallback<Document>(this, callback) {
                @Override
                protected Document parseResult() throws Exception {
                    try {
                        return XMLParser.parse(response.getText());
                    } catch (Throwable e) {
                        throw new ResponseFormatException("Response was NOT a valid XML document", e);
                    }
                }
            });
        } catch (Throwable e) {
            if (getLogger() != null) {
                getLogger()
                    .log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
            }
            callback.onFailure(this, e);
            return null;
        }
    }

    public <T extends JavaScriptObject> Object send(OverlayCallback<T> callback) {

        defaultAcceptType(Resource.CONTENT_TYPE_JSON);
        try {
            return send(new AbstractRequestCallback<T>(this, callback) {
                @SuppressWarnings("unchecked")
                @Override
                protected T parseResult() throws Exception {
                    try {
                        JSONValue val = JSONParser.parseStrict(response.getText());
                        if (val.isObject() != null) {
                            return (T) val.isObject().getJavaScriptObject();
                        } else if (val.isArray() != null) {
                            return (T) val.isArray().getJavaScriptObject();
                        } else {
                            throw new ResponseFormatException("Response was NOT a JSON object");
                        }
                    } catch (JSONException e) {
                        throw new ResponseFormatException("Response was NOT a valid JSON document", e);
                    } catch (IllegalArgumentException e) {
                        throw new ResponseFormatException("Response was NOT a valid JSON document", e);
                    }
                }
            });
        } catch (Throwable e) {
            if (getLogger() != null) {
                getLogger()
                    .log(Level.FINE, "Received http error for: " + builder.getHTTPMethod() + " " + builder.getUrl(), e);
            }
            callback.onFailure(this, e);
            return null;
        }
    }

    public Request getRequest() {
        return request;
    }

    public Response getResponse() {
        return response;
    }

    protected void defaultContentType(String type) {
        if (builder.getHeader(Resource.HEADER_CONTENT_TYPE) == null) {
            header(Resource.HEADER_CONTENT_TYPE, type);
        }
    }

    protected void defaultAcceptType(String type) {
        if (builder.getHeader(Resource.HEADER_ACCEPT) == null) {
            header(Resource.HEADER_ACCEPT, type);
        }
    }

    public Dispatcher getDispatcher() {
        return dispatcher;
    }

    public void setDispatcher(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    /**
     * add some information onto the method which could be interesting when this method
     * comes back to the dispatcher.
     *
     * @param key
     * @param value
     */
    public void addData(String key, String value) {
        data.put(key, value);
    }

    /**
     * get all data fields which was previously added
     *
     * @return
     */
    public Map<String, String> getData() {
        return data;
    }
}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.rebind;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.SelectionProperty;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.user.rebind.AbstractSourceCreator;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import static org.fusesource.restygwt.rebind.util.ClassSourceFileComposerFactoryImportUtil.addFuseSourceStaticImports;

import java.io.PrintWriter;
import java.util.HashSet;

/**
 * provides additional helper methods for generating source..
 *
 * @author Hiram Chirino
 */
public abstract class BaseSourceCreator extends AbstractSourceCreator {

    private static final int MAX_FILE_NAME_LENGTH = 200;
    public static final TreeLogger.Type ERROR = TreeLogger.ERROR;
    public static final TreeLogger.Type WARN = TreeLogger.WARN;
    public static final TreeLogger.Type INFO = TreeLogger.INFO;
    public static final TreeLogger.Type TRACE = TreeLogger.TRACE;
    public static final TreeLogger.Type DEBUG = TreeLogger.DEBUG;
    public static final TreeLogger.Type SPAM = TreeLogger.SPAM;
    public static final TreeLogger.Type ALL = TreeLogger.ALL;

    protected final GeneratorContext context;
    protected final JClassType source;
    protected final String packageName;
    protected final String shortName;
    protected final String name;
    protected SourceWriter sourceWriter;
    private TreeLogger logger;
    private PrintWriter writer;

    private static final ThreadLocal<HashSet<String>> GENERATED_CLASSES = new ThreadLocal<HashSet<String>>();

    public static HashSet<String> getGeneratedClasses() {
        HashSet<String> rc = GENERATED_CLASSES.get();
        if (rc == null) {
            rc = new HashSet<String>();
            GENERATED_CLASSES.set(rc);
        }
        return rc;
    }

    public static void clearGeneratedClasses() {
        GENERATED_CLASSES.set(null);
    }

    public static JClassType find(Class<?> type, TreeLogger logger, GeneratorContext context)
        throws UnableToCompleteException {
        return RestServiceGenerator.find(logger, context, type.getName().replace('$', '.'));
    }

    public BaseSourceCreator(TreeLogger logger, GeneratorContext context, JClassType source, String suffix) {
        this.logger = logger;
        this.context = context;
        this.source = source;
        packageName = getOpenPackageName(source.getPackage().getName());

        if (source instanceof JParameterizedType) {
            JParameterizedType ptype = (JParameterizedType) source;
            StringBuilder builder = new StringBuilder();
            for (JClassType type : ptype.getTypeArgs()) {
                builder.append("__");
                builder.append(parametersName2ClassName(type.getParameterizedQualifiedSourceName()));
            }
            shortName = reduceName(getName(source) + builder.toString() + suffix, suffix);
        } else {
            shortName = reduceName(getName(source) + suffix, suffix);
        }

        name = packageName + "." + shortName;
    }

    //Many filesystems prevent files with names larger than 256 characters.
    //Lets have class name less than 200 to allow new generators safelly to add more sufixes there if needed
    private String reduceName(String newClassName, String suffix) {
        if (newClassName.length() < MAX_FILE_NAME_LENGTH) {
            return newClassName;
        }
        //        String sufx = "_Gen_GwtJackEncDec_";
        //Lets first try to reduce the package name of the parametrized types
        // according to parametersName2ClassName parametrized types
        //Lets find if there are parametrized types

        String noSufix = newClassName.substring(0, newClassName.length() - suffix.length());
        if (newClassName.indexOf("__") > 0) {
            //has generic
            String primaryName = noSufix.substring(0, noSufix.indexOf("__"));
            String genericPart = noSufix.substring(noSufix.indexOf("__") + 2);
            StringBuilder stringBuilder = new StringBuilder();
            String[] eachGeneric = genericPart.split("__");
            for (String genericType : eachGeneric) {
                stringBuilder.append("__");
                stringBuilder.append(reduceType(genericType));
            }
            String finalName = primaryName + stringBuilder.toString() + suffix;
            if (finalName.length() > MAX_FILE_NAME_LENGTH) {
                //File name is still too long wrapping it out aggressively
                String baseName = primaryName + stringBuilder.toString();

                int firstPosition = baseName.indexOf("__");
                int lastPosition = baseName.lastIndexOf("__");
                String middle = baseName.substring(firstPosition, lastPosition);
                finalName =
                    baseName.substring(0, firstPosition) + middle.subSequence(0, 4) + "_" + (middle.length() - 5) +
                        "_" + middle.substring(middle.length() - 9) + baseName.substring(lastPosition) + suffix;
                return finalName;
            }
            return finalName;
        }
        //If there is no generic type lets give an error and force the client to reduce className
        return newClassName;
    }

    private String reduceType(String genericType) {
        if (genericType == null || !genericType.contains("_")) {
            return genericType;
        }
        String pack = genericType.substring(0, genericType.lastIndexOf("_"));
        String finalName = genericType.substring(genericType.lastIndexOf("_") + 1);
        int packSize = pack.length();
        if (packSize > 7) {
            pack = pack.subSequence(0, 2) + Integer.toString((packSize - 5)) + pack.substring(packSize - 3);
            return pack + "_" + finalName;
        }
        return genericType;
    }

    public static final String parametersName2ClassName(String parametrizedQualifiedSourceName) {
        return parametrizedQualifiedSourceName.replace('.', '_').replace("<", "__").replace(">", "__");
    }

    protected String getName(JClassType source) {
        if (source.getEnclosingType() != null) {
            return getName(source.getEnclosingType()) + "_" + source.getSimpleSourceName();
        }
        return source.getSimpleSourceName();
    }

    /**
     * Some packages are protected such that any type we generate in that package can't subsequently be loaded
     * because of a {@link SecurityException}, for example java. and javax. packages.
     *

     * To workaround this issue we add a prefix onto such packages so that the generated code can be loaded
     * later. The prefix added is open.
     *
     * @param name
     * @return
     */
    private String getOpenPackageName(String name) {
        if (name.startsWith("java.") || name.startsWith("javax.")) {
            name = "open." + name;
        }
        return name;
    }

    protected PrintWriter writer() {
        HashSet<String> classes = getGeneratedClasses();
        if (classes.contains(name)) {
            return null;
        }
        classes.add(name);
        PrintWriter writer = context.tryCreate(getLogger(), packageName, shortName);
        if (writer == null) {
            return null;
        }
        return writer;
    }

    public interface Branch<R> {
        R execute() throws UnableToCompleteException;
    }

    protected <R> R branch(String msg, Branch<R> callable) throws UnableToCompleteException {
        return branch(DEBUG, msg, callable);
    }

    protected <R> R branch(TreeLogger.Type level, String msg, Branch<R> callable) throws UnableToCompleteException {
        TreeLogger original = getLogger();
        try {
            logger = getLogger().branch(level, msg);
            return callable.execute();
        } finally {
            logger = original;
        }
    }

    public BaseSourceCreator i(int i) {
        if (i == 1) {
            sourceWriter.indent();
        } else if (i == -1) {
            sourceWriter.outdent();
        } else {
            throw new IllegalArgumentException();
        }
        return this;
    }

    public BaseSourceCreator p(String value) {
        sourceWriter.println(value);

        // System.out.println(value);
        return this;
    }

    protected BaseSourceCreator p() {
        sourceWriter.println();
        return this;
    }

    protected TreeLogger getLogger() {
        return logger;
    }

    static String join(int[] values, String sep) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < values.length; i++) {
            if (i != 0) {
                sb.append(sep);
            }
            sb.append(values[i]);
        }
        return sb.toString();
    }

    static String join(Object[] values, String sep) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < values.length; i++) {
            if (i != 0) {
                sb.append(sep);
            }
            sb.append(values[i]);
        }
        return sb.toString();
    }

    public final String create() throws UnableToCompleteException {
        writer = writer();
        if (writer == null) {
            return name;
        }
        logger = getLogger().branch(TreeLogger.DEBUG, "Generating: " + name);

        ClassSourceFileComposerFactory composerFactory = createComposerFactory();
        addFuseSourceStaticImports(composerFactory);
        sourceWriter = composerFactory.createSourceWriter(context, writer);

        generate();
        sourceWriter.commit(getLogger());
        return name;
    }

    /**
     * Returns the boolean value of the property or the default value.
     */
    protected static boolean getBooleanProperty(TreeLogger logger, PropertyOracle propertyOracle, String propertyName,
                                                boolean defaultValue) {
        try {
            SelectionProperty prop = propertyOracle.getSelectionProperty(logger, propertyName);
            String propVal = prop.getCurrentValue();
            return Boolean.parseBoolean(propVal);
        } catch (BadPropertyValueException e) {
            // return the default value
        }
        return defaultValue;
    }

    protected abstract ClassSourceFileComposerFactory createComposerFactory() throws UnableToCompleteException;

    protected abstract void generate() throws UnableToCompleteException;
}


/**
 * Copyright (C) 2009-2012 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.fusesource.restygwt.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.i18n.shared.TimeZone;

import org.fusesource.restygwt.client.dispatcher.DefaultDispatcher;

/**
 * Provides ability to set the default date format and service root (defaults to
 * GWT.getModuleBaseURL()).
 *
 *
 * @author Dave Finch
 *
 */
public class Defaults {

    private static Dispatcher dispatcher = DefaultDispatcher.INSTANCE;

    private static String serviceRoot = GWT.getModuleBaseURL();
    private static String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
    private static boolean dateFormatHasTimeZone = true;
    private static TimeZone timeZone = null;
    private static boolean ignoreJsonNulls = false;
    private static boolean addXHttpMethodOverrideHeader = true;
    // patch TNY: timeout ms,
    // if >-1, used in Method class to set timeout
    private static int requestTimeout = -1;
    private static boolean byteArraysToBase64 = false;

    private static ExceptionMapper exceptionMapper = new ExceptionMapper();

    public static String getServiceRoot() {
        return serviceRoot;
    }

    /**
     * sets the URL prepended to the value of Path annotations.
     *
     * @param serviceRoot
     */
    public static void setServiceRoot(String serviceRoot) {
        // GWT.getModuleBaseURL() is guaranteed to end with a slash, so should any custom service root
        if (!serviceRoot.endsWith("/")) {
            serviceRoot += "/";
        }
        Defaults.serviceRoot = serviceRoot;
    }

    /**
     * Gets the format used when encoding and decoding Dates. Defaults to
     * {@code "yyyy-MM-dd'T'HH:mm:ss.SSSZ"}. If the date format is set to
     * {@code null}, dates will be encoded and decoded as timestamps
     * (the number of milliseconds since the Unix epoch, 1970-01-01).
     *
     * @return the date format string
     * @see com.google.gwt.i18n.shared.DateTimeFormat DateTimeFormat
     */
    public static String getDateFormat() {
        return dateFormat;
    }

    /**
     * Sets the format used when encoding and decoding Dates. If the date
     * format is set to {@code null}, dates will be encoded and decoded as
     * timestamps (the number of milliseconds since the Unix epoch,
     * 1970-01-01).
     *
     * @param dateFormat the date format string
     * @see com.google.gwt.i18n.shared.DateTimeFormat DateTimeFormat
     */
    public static void setDateFormat(String dateFormat) {
        Defaults.dateFormat = dateFormat;
        dateFormatHasTimeZone = false;

        if (dateFormat != null) {
            for (int i = 0; i < dateFormat.length(); i++) {
                char ch = dateFormat.charAt(i);

                if (ch == 'Z' || ch == 'z' || ch == 'V' || ch == 'v') {
                    dateFormatHasTimeZone = true;
                    break;
                }
            }
        }
    }

    /* package */
    static boolean dateFormatHasTimeZone() {
        return dateFormatHasTimeZone;
    }

    /**
     * Gets the timezone used when encoding and decoding Dates.
     *

     * The timezone is only taken into consideration if the date format string
     * does not contain a timezone field. If the timezone is set to
     * {@code null}, the browser's default (local) timezone will be used.
     *
     * @return the date format timezone (null for local timezone)
     */
    public static TimeZone getTimeZone() {
        return timeZone;
    }

    /**
     * Gets the timezone used when encoding and decoding Dates.
     *

     * The timezone is only taken into consideration if the date format string
     * does not contain a timezone field. If the timezone is set to null, the
     * browser's default (local) timezone will be used.
     *
     * @param timeZone the new timezone (use null for local timezone)
     */
    public static void setTimeZone(TimeZone timeZone) {
        Defaults.timeZone = timeZone;
    }

    /**
     * Indicates whether or not nulls will be ignored during JSON marshalling.
     */
    public static boolean doesIgnoreJsonNulls() {
        return ignoreJsonNulls;
    }

    public static void ignoreJsonNulls() {
        ignoreJsonNulls = true;
    }

    public static void dontIgnoreJsonNulls() {
        ignoreJsonNulls = false;
    }

    public static final int getRequestTimeout() {
        return requestTimeout;
    }

    public static final void setRequestTimeout(int requestTimeout) {
        Defaults.requestTimeout = requestTimeout;
    }

    /**
     * @return the byteArraysToBase64
     */
    public static boolean isByteArraysToBase64() {
        return byteArraysToBase64;
    }

    /**
     * @param byteArraysToBase64 the byteArraysToBase64 to set
     */
    public static void setByteArraysToBase64(boolean byteArraysToBase64) {
        Defaults.byteArraysToBase64 = byteArraysToBase64;
    }

    /**
     * Sets the default dispatcher used by Method instances.
     *
     * @param value
     */
    public static void setDispatcher(Dispatcher value) {
        dispatcher = value;
    }

    /**
     * Returns the default dispatcher.
     *
     * @return
     */
    public static Dispatcher getDispatcher() {
        return dispatcher;
    }

    /**
     * Gets the default ExceptionMapper
     * @return
     */
    public static ExceptionMapper getExceptionMapper() {
        return exceptionMapper;
    }

    /**
     * Sets the default ExceptionMapper
     * @param exceptionMapper the new ExceptionMapper to be used by all requests
     */
    public static void setExceptionMapper(ExceptionMapper exceptionMapper) {
        Defaults.exceptionMapper = exceptionMapper;
    }

    /**
     * If true, the 'X-HTTP-Method-Override' header is set on each request.
     * @return
     */
    public static boolean isAddXHttpMethodOverrideHeader() {
        return addXHttpMethodOverrideHeader;
    }

    /**
     * If true, the 'X-HTTP-Method-Override' header is set on each request. Default is true.
     * @param addXHttpMethodOverrideHeader
     */
    public static void setAddXHttpMethodOverrideHeader(boolean addXHttpMethodOverrideHeader) {
        Defaults.addXHttpMethodOverrideHeader = addXHttpMethodOverrideHeader;
    }
}