com.lillicoder.newsblurry.net.PreferenceCookieStore.java Source code

Java tutorial

Introduction

Here is the source code for com.lillicoder.newsblurry.net.PreferenceCookieStore.java

Source

/**
 * Copyright 2012 Scott Weeden-Moody
 *
 * 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 com.lillicoder.newsblurry.net;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import junit.framework.Assert;

import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

/**
 * {@link CookieStore} implementation that uses {@link SharedPreferences}
 * as the persistent storage medium. Cookie names are used as the primary
 * ID for any given {@link Cookie}.
 * @author lillicoder
 */
public class PreferenceCookieStore implements CookieStore {

    private static final String TAG = PreferenceCookieStore.class.getSimpleName();

    private static final String EXCEPTION_INVALID_CONTEXT_PARAM = "The given context must not be null.";

    private static final String WARNING_FAILED_TO_CLOSE_ARRAY_OUTPUT_STREAM = "Failed to close array output stream!";

    private static final String WARNING_FAILED_TO_FIND_SERIALIZABLE_COOKIE_CLASS = "Failed to find SerializableCookie class when reading input stream, will return null.";

    private static final String WARNING_FAILED_TO_READ_COOKIE_FROM_STREAM = "Failed to read from input stream, will return null.";

    private static final String WARNING_FAILED_TO_WRITE_COOKIE_TO_STREAM = "Failed to write given cookie to output stream, cookie will not be persisted in preferences, will return null.";

    /**
     * Key for an individual stored cookie preference.
     */
    private static final String PREFERENCE_COOKIE_ENTRY = "cookie_%s";

    /**
     * Key for the stored cookie IDs preference. Each ID is the the cookie name
     * plus a hard-coded prefix.
     */
    private static final String PREFERENCE_COOKIE_IDS = "storedCookieIds";

    /**
     * Delimiter for cookie IDs stored in the cookie IDs preference.
     */
    private static final String PREFERENCE_COOKIE_IDS_DELIMITER = ",";

    private Context _context;
    private Map<String, Cookie> _cache;

    /**
     * Creates a new instance of this store. All persisted
     * cookies in preferences will be loaded in-memory upon creation.
     * @param context {@link Context} to access {@link SharedPreferences} with.
     */
    public PreferenceCookieStore(Context context) {
        if (context == null)
            throw new IllegalArgumentException(EXCEPTION_INVALID_CONTEXT_PARAM);

        this._context = context;
        this._cache = this.getPersistedCookies();
    }

    @Override
    public void addCookie(Cookie cookie) {
        if (cookie != null) {
            if (cookie.isExpired(new Date()))
                this.deleteCookie(cookie);
            else
                this.saveCookie(cookie);
        }
    }

    @Override
    public void clear() {
        Map<String, Cookie> cache = this.getCookieCache();

        // Clear saved cookies fist, then empty in-memory cache.
        SharedPreferences.Editor editor = this.getPreferences().edit();
        for (Cookie cookie : cache.values()) {
            String cookieId = this.getCookiePreferenceEntryId(cookie);
            editor.remove(cookieId);
        }

        editor.remove(PREFERENCE_COOKIE_IDS);
        editor.commit();

        // Clear cache.
        cache.clear();
    }

    @Override
    public boolean clearExpired(Date date) {
        boolean hasClearedCookie = false;

        SharedPreferences.Editor editor = this.getPreferences().edit();
        Map<String, Cookie> cache = this.getCookieCache();
        for (Entry<String, Cookie> cachedCookie : cache.entrySet()) {
            String name = cachedCookie.getKey();
            Cookie cookie = cachedCookie.getValue();
            if (cookie.isExpired(new Date())) {
                // Clear from in-memory cache.
                cache.remove(name);

                // Clear from persistent storage.
                String cookieId = this.getCookiePreferenceEntryId(cookie);
                editor.remove(cookieId);

                hasClearedCookie = true;
            }
        }

        // Save changes for persistent storage.
        editor.commit();

        // If we have cleared any cookie, we must update the
        // current cookie IDs in persistent storage.
        if (hasClearedCookie)
            this.updateCookieIds(cache.keySet());

        return hasClearedCookie;
    }

    /**
     * Determines if this store contains a cookie with the given cookie name.
     * @param cookieName Name of the cookie to search for.
     * @return <code>true</code> if this store contains a cookie with the given name,
     *          <code>false</code> otherwise.
     */
    public boolean containsCookie(String cookieName) {
        Map<String, Cookie> cache = this.getCookieCache();
        return cache.containsKey(cookieName);
    }

    @Override
    public List<Cookie> getCookies() {
        Map<String, Cookie> cache = this.getCookieCache();
        return new ArrayList<Cookie>(cache.values());
    }

    /**
     * Deletes the given {@link Cookie} from this store's
     * in-memory cache and persistent storage. If the cookie
     * is not currently stored, this method does nothing.
     * @param cookie {@link Cookie} to delete from cache and storage.
     */
    private void deleteCookie(Cookie cookie) {
        Assert.assertTrue(cookie != null);

        Map<String, Cookie> cache = this.getCookieCache();
        cache.remove(cookie.getName());

        String cookieId = this.getCookiePreferenceEntryId(cookie);

        SharedPreferences.Editor editor = this.getPreferences().edit();
        editor.remove(cookieId);
        editor.commit();

        // We have removed a cookie, we must update the ID set with current valid cookie IDs.
        this.updateCookieIds(cache.keySet());
    }

    /**
     * Decodes the given base-64 cookie string and returns a
     * {@link Cookie} representing that data.
     * @param encodedCookie Base-64 encoded string of a stored {@link Cookie}.
     * @return {@link Cookie} decoded from the given base-64 encoded cookie string.
     */
    private Cookie decodeCookie(String encodedCookie) {
        Assert.assertTrue(encodedCookie != null);

        byte[] rawData = Base64.decode(encodedCookie, Base64.DEFAULT);
        ByteArrayInputStream in = new ByteArrayInputStream(rawData);

        Cookie cookie = null;
        try {
            ObjectInputStream inStream = new ObjectInputStream(in);
            SerializableCookie serializableCookie = (SerializableCookie) inStream.readObject();
            if (serializableCookie != null)
                cookie = serializableCookie.getCookie();
        } catch (ClassNotFoundException e) {
            Log.w(TAG, WARNING_FAILED_TO_FIND_SERIALIZABLE_COOKIE_CLASS);
        } catch (IOException e) {
            Log.w(TAG, WARNING_FAILED_TO_READ_COOKIE_FROM_STREAM);
        }

        return cookie;
    }

    /**
     * Encodes the given {@link SerializableCookie} as a base-64 encoded string.
     * This string is what will be persisted to preferences.
     * @param cookie {@link SerializableCookie} to encode.
     * @return Encoded base-64 string for the given cookie,
     *          <code>null</code> if we failed to serialize the given cookie.
     */
    private String encodeCookie(Cookie cookie) {
        Assert.assertTrue(cookie != null);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ObjectOutputStream outStream = new ObjectOutputStream(out);

            SerializableCookie wrappedCookie = new SerializableCookie(cookie);
            outStream.writeObject(wrappedCookie);

            outStream.close();
        } catch (IOException e) {
            Log.w(TAG, WARNING_FAILED_TO_WRITE_COOKIE_TO_STREAM);
            return null;
        }

        try {
            out.close();
        } catch (IOException e) {
            Log.w(TAG, WARNING_FAILED_TO_CLOSE_ARRAY_OUTPUT_STREAM, e);
        }

        return Base64.encodeToString(out.toByteArray(), Base64.DEFAULT);
    }

    /**
     * Gets the {@link Context} for this cookie store.
     * @return {@link Context} for this cookie store.
     */
    private Context getContext() {
        return this._context;
    }

    /**
     * Gets the in-memory cache of {@link Cookie} for this store.
     * @return In-memory {@link Map} of {@link Cookie} keyed by name.
     */
    private Map<String, Cookie> getCookieCache() {
        return this._cache;
    }

    /**
     * Gets the cookie preference entry ID for the given {@link Cookie}.
     * @param cookie {@link Cookie} to get the preference ID of.
     * @return Cookie preference entry ID.
     */
    private String getCookiePreferenceEntryId(Cookie cookie) {
        return String.format(PREFERENCE_COOKIE_ENTRY, cookie.getName());
    }

    /**
     * Gets all persisted cookies stored in {@link SharedPreferences}.
     * @return {@link Map} of {@link Cookie} where each entry is keyed by name.
     */
    private Map<String, Cookie> getPersistedCookies() {
        Map<String, Cookie> cookies = new ConcurrentHashMap<String, Cookie>();

        SharedPreferences preferences = this.getPreferences();
        String storedCookieIds = preferences.getString(PREFERENCE_COOKIE_IDS, null);
        if (storedCookieIds != null) {
            String[] cookieIds = storedCookieIds.split(PREFERENCE_COOKIE_IDS_DELIMITER);
            for (String id : cookieIds) {
                String cookieId = String.format(PREFERENCE_COOKIE_ENTRY, id);
                String encodedCookie = preferences.getString(cookieId, null);
                if (encodedCookie != null) {
                    Cookie cookie = this.decodeCookie(encodedCookie);
                    if (cookie != null)
                        cookies.put(id, cookie);
                }
            }
        }

        return cookies;
    }

    /**
     * Gets an instance of {@link SharedPreferences} for 
     * use in storing and retrieving cookies.
     * @return {@link SharedPreferences} instance for this store.
     */
    private SharedPreferences getPreferences() {
        Context context = this.getContext();
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    /**
     * Saves the given {@link Cookie} to this store's
     * in-memory cache and persistent storage.
     * @param cookie {@link Cookie} to save to cache and storage.
     */
    private void saveCookie(Cookie cookie) {
        Assert.assertTrue(cookie != null);

        Map<String, Cookie> cache = this.getCookieCache();
        cache.put(cookie.getName(), cookie);

        String cookieId = this.getCookiePreferenceEntryId(cookie);
        String encodedCookie = this.encodeCookie(cookie);

        SharedPreferences.Editor editor = this.getPreferences().edit();
        editor.putString(cookieId, encodedCookie);
        editor.commit();

        // We have added a cookie, we must update the ID set with the current valid cookie IDs.
        this.updateCookieIds(cache.keySet());
    }

    /**
     * Updates this store's persistent index of cookie IDs to contain
     * the given collection of cookie IDs.
     */
    private void updateCookieIds(Set<String> cookieIds) {
        SharedPreferences.Editor editor = this.getPreferences().edit();

        String joinedCookieIds = TextUtils.join(PREFERENCE_COOKIE_IDS_DELIMITER, cookieIds);
        editor.putString(PREFERENCE_COOKIE_IDS, joinedCookieIds);

        editor.commit();
    }

}