edu.usf.cutr.opentripplanner.android.util.JacksonConfig.java Source code

Java tutorial

Introduction

Here is the source code for edu.usf.cutr.opentripplanner.android.util.JacksonConfig.java

Source

/*
 * Copyright 2013 University of South Florida
 * 
 * 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 edu.usf.cutr.opentripplanner.android.util;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

import org.opentripplanner.api.ws.Response;

import android.content.Context;
import android.util.Log;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.DecimalFormat;

import static edu.usf.cutr.opentripplanner.android.OTPApp.TAG;

/**
 * This class holds a static instance of a Jackson ObjectMapper and ObjectReader
 * that are configured for parsing server JSON responses.
 *
 * The ObjectMapper, ObjectReader, and XmlMapper are thread-safe after it is
 * configured: http://wiki.fasterxml.com/JacksonFAQThreadSafety
 *
 * ...so we can configure it once here and then use it in multiple fragments.
 *
 * @author Sean J. Barbeau
 */
public class JacksonConfig {

    // For JSON
    private static ObjectMapper mapper = null;

    private static ObjectReader reader = null;

    // For caching objects (ObjectMapper, ObjectReader, and XmlMapper) if
    // desired
    private static Context context = null;

    // Used to time cache read and write
    private static long cacheReadStartTime = 0;

    private static long cacheReadEndTime = 0;

    private static long cacheWriteStartTime = 0;

    private static long cacheWriteEndTime = 0;

    private static boolean usingCache = false;

    // Constants for defining which object type to read/write from/to cache
    private static final String OBJECT_READER = "ObjectReader";

    private static final String OBJECT_MAPPER = "ObjectMapper";

    private static final String CACHE_FILE_EXTENSION = ".cache";

    // Used to format decimals to 3 places
    static DecimalFormat df = new DecimalFormat("#,###.###");

    // Private empty constructor since this object shouldn't be instantiated
    private JacksonConfig() {
    }

    /**
     * Returns true if the application is using a cache to read/write serialized
     * Jackson ObjectMapper/ObjectReader/XmlMapper to reduce cold-start latency,
     * false if it is not
     *
     * @return true if the application is using a cache to read/write serialized
     * Jackson ObjectMapper/ObjectReader/XmlMapper to reduce cold-start
     * latency, false if it is not
     */
    public static boolean isUsingCache() {
        // Check to see if the context is null. If it is, we can't cache data.
        if (usingCache && context != null) {
            return true;
        } else {
            if (!usingCache || context == null) {
                return false;
            }
        }

        // Should never reach here
        return usingCache;
    }

    /**
     * True if the application should use a cache to read/write serialized
     * Jackson ObjectMapper/ObjectReader/XmlMapper to reduce cold-start latency,
     * false if it should not
     *
     * @param usingCache True if the application should use a cache to read/write
     *                   serialized Jackson ObjectMapper/ObjectReader/XmlMapper to
     *                   reduce cold-start latency, false if it should not
     * @param context    Context that should be used to access the cache location.
     *                   getApplicationContext() is suggested, since the Jackson
     *                   Objects are thread-safe and static
     */
    public static void setUsingCache(boolean usingCache, Context context) {
        JacksonConfig.usingCache = usingCache;
        JacksonConfig.context = context;
    }

    /**
     * Returns a benchmark of the amount of time the last cache read took for
     * the ObjectMapper or ObjectReader or XmlReader (in nanoseconds)
     *
     * @return a benchmark of the amount of time the last cache read took for
     * the ObjectMapper or ObjectReader or XmlReader (in nanoseconds)
     */
    public static long getLastCacheReadTime() {
        return cacheReadEndTime - cacheReadStartTime;
    }

    /**
     * Returns a benchmark of the amount of time the last cache write took for
     * the ObjectMapper or ObjectReader or XmlReader (in nanoseconds)
     *
     * @return a benchmark of the amount of time the last cache write took for
     * the ObjectMapper or ObjectReader or XmlReader (in nanoseconds)
     */
    public static long getLastCacheWriteTime() {
        return cacheWriteEndTime - cacheWriteStartTime;
    }

    /**
     * Constructs a thread-safe instance of a Jackson ObjectMapper configured to
     * parse JSON responses from a OTP REST API.
     *
     * According to Jackson Best Practices
     * (http://wiki.fasterxml.com/JacksonBestPracticesPerformance), for
     * efficiency reasons you should use the ObjectReader (via
     * getObjectReaderInstance()) instead of the ObjectMapper.
     *
     * @return thread-safe ObjectMapper configured for OTP JSON responses
     * @deprecated
     */
    public synchronized static ObjectMapper getObjectMapperInstance() {
        return initObjectMapper();
    }

    /**
     * Constructs a thread-safe instance of a Jackson ObjectReader configured to
     * parse JSON responses from a Mobile OTP API
     *
     * According to Jackson Best Practices
     * (http://wiki.fasterxml.com/JacksonBestPracticesPerformance), this should
     * be more efficient than the ObjectMapper.
     *
     * @return thread-safe ObjectMapper configured for OTP JSON responses
     */
    public synchronized static ObjectReader getObjectReaderInstance() {
        if (reader == null) {
            /**
             * We don't have a reference to an ObjectReader, so we need to read
             * from cache or instantiate a new one
             */
            if (usingCache) {
                reader = (ObjectReader) readFromCache(OBJECT_READER);

                if (reader != null) {
                    // Successful read from the cache
                    return reader;
                }
            }

            /**
             * If we reach this point then we're either not reading from the
             * cache, there was nothing in the cache to retrieve, or there was
             * an error reading from the cache.
             *
             * Instantiate the object like normal.
             */
            reader = initObjectMapper().reader(Response.class);
        }
        return reader;
    }

    /**
     * Internal method used to init main ObjectMapper for JSON parsing
     *
     * @return initialized ObjectMapper ready for JSON parsing
     */
    private static ObjectMapper initObjectMapper() {
        if (mapper == null) {
            /**
             * We don't have a reference to an ObjectMapper, so we need to read
             * from cache or instantiate a new one
             */
            if (usingCache) {
                mapper = (ObjectMapper) readFromCache(OBJECT_MAPPER);

                if (mapper != null) {
                    // Successful read from the cache
                    return mapper;
                }
            }

            /**
             * If we reach this point then we're either not reading from the
             * cache, there was nothing in the cache to retrieve, or there was
             * an error reading from the cache.
             *
             * Instantiate the object like normal.
             */
            // Jackson configuration
            mapper = new ObjectMapper();

            mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
            mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
            mapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true);
            mapper.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }
        return mapper;
    }

    /**
     * Forces the write of a ObjectMapper or ObjectReader to the app
     * cache. The cache is used to reduce the cold-start delay for Jackson
     * parsing on future runs, after this VM instance is destroyed.
     *
     * Applications may call this after a JSON or XML call to the server to
     * attempt to hide the cache write latency from the user, instead of having
     * the cache write occur as part of the first request to use the
     * ObjectMapper or ObjectReader.
     *
     *
     * This method is non-blocking.
     *
     * @param object object to be written to the cache
     */
    public static void forceCacheWrite(final Serializable object) {
        if (isUsingCache()) {
            new Thread() {
                public void run() {
                    writeToCache(object);
                }

                ;
            }.start();
        } else {
            Log.w(TAG,
                    "App tried to force a cache write but caching is not activated.  If you want to use the cache, call JacksonConfig.setUsingCache(true, context) with a reference to your context.");
        }
    }

    /**
     * Forces the read of a ObjectMapper or ObjectReader from the
     * app cache to be stored as a static instance in this object. The cache is
     * used to reduce the cold-start delay for Jackson parsing on future runs,
     * after this VM instance is destroyed.
     *
     * Applications should call this on startup to attempt to hide the cache
     * read latency from the user, instead of having the cache read occur on the
     * first request to use the ObjectMapper or ObjectReader.
     *
     * This method is non-blocking.
     */
    public static void forceCacheRead() {
        if (isUsingCache()) {
            new Thread() {
                public void run() {
                    readFromCache(OBJECT_MAPPER);
                    readFromCache(OBJECT_READER);
                }

                ;
            }.start();
        } else {
            Log.w(TAG,
                    "App tried to force a cache write but caching is not activated.  If you want to use the cache, call JacksonConfig.setUsingCache(true, context) with a reference to your context.");
        }
    }

    /**
     * Write the given object to Android internal storage for this app
     *
     * @param object serializable object to be written to cache (ObjectReader,
     *               ObjectMapper, or XmlReader)
     * @return true if object was successfully written to cache, false if it was
     * not
     */
    private synchronized static boolean writeToCache(Serializable object) {

        FileOutputStream fileStream = null;
        ObjectOutputStream objectStream = null;
        String fileName = "";
        boolean success = false;

        if (context != null) {
            try {
                if (object instanceof ObjectMapper) {
                    fileName = OBJECT_MAPPER + CACHE_FILE_EXTENSION;
                }
                if (object instanceof ObjectReader) {
                    fileName = OBJECT_READER + CACHE_FILE_EXTENSION;
                }

                cacheWriteStartTime = System.nanoTime();
                fileStream = context.openFileOutput(fileName, Context.MODE_PRIVATE);
                objectStream = new ObjectOutputStream(fileStream);
                objectStream.writeObject(object);
                objectStream.flush();
                fileStream.getFD().sync();
                cacheWriteEndTime = System.nanoTime();
                success = true;

                // Get size of serialized object
                long fileSize = context.getFileStreamPath(fileName).length();

                Log.d("TAG", "Wrote " + fileName + " to cache (" + fileSize + " bytes) in "
                        + df.format(getLastCacheWriteTime()) + " ms.");
            } catch (IOException e) {
                // Reset timestamps to show there was an error
                cacheWriteStartTime = 0;
                cacheWriteEndTime = 0;
                Log.e(TAG, "Couldn't write Jackson object '" + fileName + "' to cache: " + e);
            } finally {
                try {
                    if (objectStream != null) {
                        objectStream.close();
                    }
                    if (fileStream != null) {
                        fileStream.close();
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Error closing file connections: " + e);
                }
            }
        } else {
            Log.w(TAG,
                    "Can't write to cache - no context provided.  If you want to use the cache, call JacksonConfig.setUsingCache(true, context) with a reference to your context.");
        }

        return success;
    }

    /**
     * Read the given object from Android internal storage for this app
     *
     * @param objectType object type, defined by class constant Strings, to retrieve
     *                   from cache (ObjectReader, ObjectMapper, or XmlReader)
     * @return deserialized Object, or null if object couldn't be deserialized
     */
    private static synchronized Serializable readFromCache(String objectType) {

        FileInputStream fileStream = null;
        ObjectInputStream objectStream = null;

        // Holds object to be read from cache
        Serializable object = null;

        // Before reading from cache, check to make sure that we don't already
        // have the requested object in memory
        if (objectType.equalsIgnoreCase(OBJECT_MAPPER) && mapper != null) {
            return mapper;
        }
        if (objectType.equalsIgnoreCase(OBJECT_READER) && reader != null) {
            return reader;
        }

        if (context != null) {
            try {
                String fileName = objectType + CACHE_FILE_EXTENSION;

                cacheReadStartTime = System.nanoTime();
                fileStream = context.openFileInput(fileName);
                objectStream = new ObjectInputStream(fileStream);
                object = (Serializable) objectStream.readObject();
                cacheReadEndTime = System.nanoTime();

                // Get size of serialized object
                long fileSize = context.getFileStreamPath(fileName).length();

                Log.d("TAG", "Read " + fileName + " from cache (" + fileSize + " bytes) in "
                        + df.format(getLastCacheReadTime()) + " ms.");
            } catch (FileNotFoundException e) {
                Log.w(TAG, "Cache miss - Jackson object '" + objectType + "' does not exist in app cache: " + e);
                return null;
            } catch (Exception e) {
                // Reset timestamps to show there was an error
                cacheReadStartTime = 0;
                cacheReadEndTime = 0;
                Log.e(TAG, "Couldn't read Jackson object '" + objectType + "' from cache: " + e);
            } finally {
                try {
                    if (objectStream != null) {
                        objectStream.close();
                    }
                    if (fileStream != null) {
                        fileStream.close();
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Error closing cache file connections: " + e);
                }
            }

            if (object instanceof ObjectMapper) {
                mapper = (ObjectMapper) object;
            }
            if (object instanceof ObjectReader) {
                reader = (ObjectReader) object;
            }

            return object;
        } else {
            Log.w(TAG,
                    "Couldn't read from cache - no context provided.  If you want to use the cache, call JacksonConfig.setUsingCache(true, context) with a reference to your context.");
            return null;
        }
    }
}