Java tutorial
/* * 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; } } }