GSONBuilderService.java

    package org.apache.fulcrum.json.gson;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.concurrent.Callable;

import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.fulcrum.json.JsonService;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.GsonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.GsonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;

/**
 * 
 * By default multiple serialization of the same object in a single thread is
 * not support (e.g adapter + default for the same bean / object).
 * 
 * 
 * @author gk
 * @version $Id$
 * 
 */
public class GSONBuilderService extends AbstractLogEnabled implements
        JsonService, Initializable, Configurable {

    private static final String GLOBAL_ADAPTERS = "globalAdapters";

    private static final String DATE_FORMAT = "dateFormat";
    
    private static final String USEJSONPATH = "useJsonPath";

    private String dateFormat;

    private Hashtable<String, String> adapters = null;

    private boolean useJsonPath = false;
    
    GsonBuilder gson;

    @Override
    public String ser(Object src) throws Exception {
        getLogger().debug("ser" + src);
        return gson.create().toJson(src);
    }

    @Override
    public <T> String ser(Object src, Class<T> type) throws Exception {
        getLogger().debug("ser::" + src + " with type" + type);

        Type collectionType = new TypeToken<T>() {
        }.getType();
        return gson.create().toJson(src, collectionType);
    }

    @Override
    public <T> T deSer(String json, Class<T> type) throws Exception {
        // TODO Auto-generated method stub
        getLogger().debug("deser:" + json);
        return gson.create().fromJson(json, type);
    }
    
    @Override
    public <T> Collection<T> deSerCollection(String json, Object collectionType,
            Class<T> elementType) throws Exception {
        getLogger().debug("deser:" + json);
        getLogger().debug("collectionType:" + collectionType);
        return  gson.create().fromJson(json, (Type)collectionType);
    }

    @Override
    public String serializeOnlyFilter(Object src, String... filterAttr)
            throws Exception {
        return  gson
                .addSerializationExclusionStrategy(
                        include(null,filterAttr)).create().toJson(src);
    }

    @Override
    public String serializeOnlyFilter(Object src, Boolean notused,
            String... filterAttr) throws Exception {
        return  gson
                .addSerializationExclusionStrategy(
                        include(null,filterAttr)).create().toJson(src);
    }

    @Override
    public <T> String serializeOnlyFilter(Object src, Class<T> filterClass,
            String... filterAttr) throws Exception {
        return  gson
        .addSerializationExclusionStrategy(
                include(filterClass, filterAttr)).create().toJson(src);
    }
    
    @Override
    public <T> String serializeOnlyFilter(Object arg0, Class<T> arg1,
            Boolean arg2, String... arg3) throws Exception {
        throw new Exception("Not yet implemented!");
    }

    /**
     * registering an adapter 
     * 
     * @see GsonBuilder#registerTypeAdapter(Type, Object)
     */
    @Override
    public JsonService addAdapter(String name, Class target, Object adapter)
            throws Exception {
        gson.registerTypeAdapter(target, adapter);
        return this;
    }

    /**
     * registering an adapter. Unregistering could be only done by reinitialize {@link GsonBuilder} 
     * using @link {@link GSONBuilderService#initialize()}, although a new Adapter with the same target overwrites the previously defined.
     * 
     * @see GsonBuilder#registerTypeAdapter(Type, Object)
     */
    @Override
    public JsonService addAdapter(String name, Class target, Class adapter)
            throws Exception {
        gson.registerTypeAdapter(target, adapter.getConstructor().newInstance());
        return null;
    }

    @Override
    public <T> String serializeAllExceptFilter(Object src,
            Class<T> filterClass, String... filterAttr) throws Exception {
        return gson
                .addSerializationExclusionStrategy(
                        exclude(filterClass, filterAttr)).create().toJson(src);
    }
    
    @Override
    public <T> String serializeAllExceptFilter(Object src, Class<T> filterClass,
            Boolean clearCache, String... filterAttr) throws Exception {
        throw new Exception("Not yet implemented!");
    }
    
    @Override
    public String serializeAllExceptFilter(Object src, String... filterAttr)
            throws Exception {
        return gson
                .addSerializationExclusionStrategy(
                        exclude(null, filterAttr)).create().toJson(src);
    }

    @Override
    public String serializeAllExceptFilter(Object src, Boolean notused,
            String... filterAttr) throws Exception {
        return gson
                .addSerializationExclusionStrategy(
                        exclude(null, filterAttr)).create().toJson(src);
    }
    
    @Override
    public String ser(Object src, Boolean refreshCache) throws Exception {
        throw new Exception("Not implemented!");
    }

    @Override
    public <T> String ser(Object src, Class<T> type, Boolean refreshCache)
            throws Exception {
        throw new Exception("Not implemented!");
    }

    public JsonService registerTypeAdapter(Object serdeser, Type type) {
        gson.registerTypeAdapter(type, serdeser);
        return this;
    }
    
    /**
     * Alternative method to calling {@link #registerTypeAdapter(Object, Type)}
     * Note: Always use either this direct format call or the other adapter register call,
     * otherwise inconsistencies may occur!
     * 
     * @param dfStr date format string
     */
    public void setDateFormat(final String dfStr) {
        gson.setDateFormat(dfStr);
    }

    /* (non-Javadoc)
     * @see org.apache.fulcrum.json.JsonService#setDateFormat(java.text.DateFormat)
     */
    @Override
    public void setDateFormat(final DateFormat df) {
        DateTypeAdapter dateTypeAdapter = new DateTypeAdapter();
        dateTypeAdapter.setCustomDateFormat(df);
        gson.registerTypeAdapter(Date.class,dateTypeAdapter);
    }

    public void getJsonService() throws InstantiationException {
        // gson.registerTypeAdapter(Date.class, ser).
        // addSerializationExclusionStrategy( exclude(ObjectKey.class) ).
        // addSerializationExclusionStrategy( exclude(ComboKey.class) );
        // return gson.create().toJson( src );
    }

    /* (non-Javadoc)
     * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
     */
    @Override
    public void configure(Configuration conf) throws ConfigurationException {

        getLogger().debug("conf.getName()" + conf.getName());
        final Configuration configuredDateFormat = conf.getChild(DATE_FORMAT,
                false);
        if (configuredDateFormat != null) {
            this.dateFormat = configuredDateFormat.getValue();// DEFAULTDATEFORMAT);
        }
        final Configuration configuredAdapters = conf.getChild(GLOBAL_ADAPTERS,
                true);
        if (configuredAdapters != null) {
            Configuration[] nameVal = configuredAdapters.getChildren();
            for (int i = 0; i < nameVal.length; i++) {
                String key = nameVal[i].getName();
                getLogger().debug("configured key: " + key);
                if (key.equals("adapter")) {
                    String forClass = nameVal[i].getAttribute("forClass");
                    this.adapters = new Hashtable<String, String>();
                    this.adapters.put(forClass, nameVal[i].getValue());
                }
            }
        }
        // TODO provide configurable Type Adapters
        final Configuration configuredjsonPath = conf.getChild(
                USEJSONPATH, false);
        if (configuredjsonPath != null) {
            this.useJsonPath  = configuredjsonPath.getValueAsBoolean();
        }
    }

    /* (non-Javadoc)
     * @see org.apache.avalon.framework.activity.Initializable#initialize()
     */
    @Override
    public void initialize() throws Exception {
        gson = new GsonBuilder();
        getLogger().debug("initialized: gson:" + gson);
        if (dateFormat != null) {
            getLogger().info("setting date format to: " + dateFormat);
            setDateFormat(new SimpleDateFormat(dateFormat));
            //setDateFormat(dateFormat);
        }

        if (adapters != null) {
            Enumeration<String> enumKey = adapters.keys();
            while (enumKey.hasMoreElements()) {
                String forClass = enumKey.nextElement();
                String avClass = adapters.get(forClass);
                if (avClass != null) {
                    try {
                        getLogger().debug(
                                "initializing: adapters " + avClass
                                        + " forClass:" + forClass);
                        Class adapterForClass = Class.forName(forClass);
                        Class adapterClass = Class.forName(avClass);
                        addAdapter("Test Adapter", adapterForClass,
                                adapterClass);

                    } catch (Exception e) {
                        throw new InstantiationException(
                                "JsonMapperService: Error instantiating one of the adapters: "
                                        + avClass + " for " + forClass);
                    }
                }
            }
        }
        
        if (useJsonPath) {
            // set it before runtime
            com.jayway.jsonpath.Configuration.setDefaults(new com.jayway.jsonpath.Configuration.Defaults() {
                
                private Callable<Gson> gsonFuture = new Callable<Gson>() {
                    @Override
                    public Gson call() {
                        return GSONBuilderService.this.gson.create();
                    }
                };

                private final JsonProvider jsonProvider = new GsonJsonProvider(GSONBuilderService.this.gson.create());
                private final MappingProvider mappingProvider = new GsonMappingProvider(gsonFuture);

                @Override
                public JsonProvider jsonProvider() {
                    return jsonProvider;
                }

                @Override
                public MappingProvider mappingProvider() {
                    return mappingProvider;
                }

                @Override
                public Set<Option> options() {
                    return EnumSet.noneOf(Option.class);
                }
            });
        }
    }

    /**
     * Simple Exclusion strategy to filter class or fields used by this service
     * for serialization (not yet deserialization).
     * 
     * @param clazz
     *            The class to be filtered out.
     * @param filterAttrs
     *            The fieldnames to be filtered as string
     * @return the strategy applied by GSON
     */
    private ExclusionStrategy exclude(Class clazz, String... filterAttrs) {
        return new ExclusionStrategy() {

            public Class<?> excludedThisClass;
            public HashSet<String> excludedAttributes;

            private ExclusionStrategy init(Class<?> excludedThisClass,
                    String... filterAttrs) {
                this.excludedThisClass = excludedThisClass;
                if (filterAttrs != null) {
                    this.excludedAttributes = new HashSet<String>(
                            filterAttrs.length);
                    Collections.addAll(this.excludedAttributes, filterAttrs);
                } else
                    this.excludedAttributes = new HashSet<String>();

                return this;
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return (excludedThisClass != null) ? excludedThisClass
                        .equals(clazz) : false;
            }

            @Override
            public boolean shouldSkipField(FieldAttributes paramFieldAttributes) {
                // return paramFieldAttributes.getDeclaringClass() ==
                // excludedThisClass &&
                // excludesAttributes.contains(paramFieldAttributes.getName());
                return !excludedAttributes.isEmpty() ? this.excludedAttributes
                        .contains(paramFieldAttributes.getName()) : false;
            }
        }.init(clazz, filterAttrs);
    }
    
    /**
     * @param clazz the class to exclude
     * @param filterAttrs bean elements not to be serialized
     * @return
     */
    private ExclusionStrategy include(Class clazz, String... filterAttrs) {
        return new ExclusionStrategy() {

            private Class<?> includeThisClass;
            private HashSet<String> includedAttributes;

            private ExclusionStrategy init(Class<?> includeThisClass,
                    String... filterAttrs) {
                this.includeThisClass = includeThisClass;
                if (filterAttrs != null) {
                    this.includedAttributes = new HashSet<String>(
                            filterAttrs.length);
                    getLogger().debug(" ... adding includedAttributes:" + filterAttrs.length);
                    Collections.addAll(this.includedAttributes, filterAttrs);
                    for (String includedAttribute : includedAttributes) {
                        getLogger().debug("includedAttribute:" +includedAttribute);
                    }
                } else
                    this.includedAttributes = new HashSet<String>();

                return this;
            }

            /**
             * skip is current class is not equal provided class
             */
            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                getLogger().debug(includeThisClass+ ": comparing include class:" + clazz);
                return includeThisClass != null ? !includeThisClass
                        .equals(clazz) : false;
            }

            /**
             * skip if current field attribute is not included are skip else
             */
            @Override
            public boolean shouldSkipField(FieldAttributes paramFieldAttributes) { 
                return !includedAttributes.isEmpty() ? !this.includedAttributes
                        .contains(paramFieldAttributes.getName()) : true;        

            }
        }.init(clazz, filterAttrs);
    }

}