org.sakaiproject.nakamura.api.lite.StorageClientUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.nakamura.api.lite.StorageClientUtils.java

Source

/*
 * Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF 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.
 */
package org.sakaiproject.nakamura.api.lite;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Maps;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.content.Content;
import org.sakaiproject.nakamura.api.lite.content.ContentManager;
import org.sakaiproject.nakamura.api.lite.util.Type1UUID;
import org.sakaiproject.nakamura.lite.types.Types;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;

/**
 * Utilites for managing storage related to the Sparse Map Content Store.
 */
public class StorageClientUtils {

    /**
     * UTF8 charset constant
     */
    public final static String UTF8 = "UTF-8";
    /** how are numbers encoded, base ? */
    public final static int ENCODING_BASE = 10;
    /**
     * Default hashing algorithm for passwords
     */
    public final static String SECURE_HASH_DIGEST = "SHA-512";
    /**
     * Charset for encoding byte data as char
     * @deprecated not of any use for encoding, use encode(byte[])
     */
    public static final char[] URL_SAFE_ENCODING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
            .toCharArray();
    /**
     * Based on JackRabbit: Jackrabbit uses a subset of 8601 (8601:2000) for
     * their date times.
     */
    public static String ISO8601_JCR_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ";
    @SuppressWarnings("unused")
    private final static FastDateFormat ISO8601_JCR_FORMAT = FastDateFormat.getInstance(ISO8601_JCR_PATTERN,
            TimeZone.getTimeZone("UTC"), Locale.ENGLISH);

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageClientUtils.class);

    /**
     * Concert a storage object to string. In the sparse store everything is
     * stored in as an efficient way as possible. To avoid unnecessary
     * marshalling to and from the store we leave it to the application to
     * marshal properties in and out of the store on demand.
     * 
     * @param object
     *            the storage object
     * @return a string representation of the storage object.
     * @deprecated the application code should convert to a string if necessary,
     *             this is not required for storage any more.
     */
    @Deprecated
    public static String toString(Object object) {
        try {
            if (object instanceof String) {
                return (String) object;
            } else if (object == null || object instanceof RemoveProperty) {
                return null;
            } else if (object instanceof byte[]) {
                return new String((byte[]) object, UTF8);
            } else {
                LOGGER.warn("Converting " + object.getClass() + " to String via toString");
                return String.valueOf(object);
            }
        } catch (UnsupportedEncodingException e) {
            return null; // no utf8.. get real!
        }
    }

    /**
     * Get the name of an alternative field for an alternative stream.
     * 
     * @param field
     *            the fieldname
     * @param streamId
     *            the alternative stream name
     * @return the alternative field name.
     */
    public static String getAltField(String field, String streamId) {
        if (streamId == null) {
            return field;
        }
        return field + "/" + streamId;
    }

    /**
     * Convert the object from application to store format. This should be used
     * whenever a value is being placed into the store.
     * 
     * @param object
     *            the object to place in store.
     * @return the Store representation of the object.
     * @deprecated Objects do not need to be converted when placing in the
     *             store, provided they are one of the ones listed in
     *             {@link Types.ALLTYPES}. If they are not your code should
     *             convert to one or more of those types.
     */
    @Deprecated
    public static Object toStore(Object object) {
        return object;
    }

    /**
     * Convert a store object to a byte[]
     * 
     * @param value
     *            the store object
     * @return a byte[] of the store object.
     * @deprecated if its a byte[] just use it as a byte[] otherwise convert to
     *             a byte[] before storing. eg
     *             String.valueOf(value).getBytes("UTF-8")
     */
    @Deprecated
    public static byte[] toBytes(Object value) {
        if (value instanceof byte[]) {
            return (byte[]) value;
        } else {
            try {
                return String.valueOf(value).getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                return null; // no utf8.. get real!
            }
        }
    }

    /**
     * @param objectPath
     * @return true if the objectPath represents a root path.
     */
    public static boolean isRoot(String objectPath) {
        return (objectPath == null) || "/".equals(objectPath) || "".equals(objectPath)
                || (objectPath.indexOf("/") < 0);
    }

    /**
     * @param objectPath
     * @return the parent of the supplied path or the path if already a root.
     */
    public static String getParentObjectPath(String objectPath) {
        if ("/".equals(objectPath)) {
            return "/";
        }
        int i = objectPath.lastIndexOf('/');
        if (i == objectPath.length() - 1) {
            i = objectPath.substring(0, i).lastIndexOf('/');
        }
        String res = objectPath;
        if (i > 0) {
            res = objectPath.substring(0, i);
        } else if (i == 0) {
            return "/";
        }
        return res;
    }

    /**
     * @param objectPath
     * @return the name of the supplied path, normally defiend as the last
     *         element in the path.
     */
    public static String getObjectName(String objectPath) {
        if ("/".equals(objectPath)) {
            return "/";
        }
        int i = objectPath.lastIndexOf('/');
        int j = objectPath.length();
        if (i == objectPath.length() - 1) {
            j--;
            i = objectPath.substring(0, i).lastIndexOf('/');
        }
        String res = objectPath;
        if (i >= 0) {
            res = objectPath.substring(i + 1, j);
        }
        return res;

    }

    /**
     * @param naked
     * @return a lower cost insecure hash of the naked value which can be used
     *         for keys as its not too long.
     */
    // TODO: Unit test
    public static String insecureHash(String naked) {
        try {
            return insecureHash(naked.getBytes(UTF8));
        } catch (UnsupportedEncodingException e3) {
            LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
            return null;
        }
    }

    public static String insecureHash(byte[] b) {
        try {
            MessageDigest md;
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e1) {
                try {
                    md = MessageDigest.getInstance("MD5");
                } catch (NoSuchAlgorithmException e2) {
                    LOGGER.error(
                            "You have no Message Digest Algorightms intalled in this JVM, secure Hashes are not availalbe, encoding bytes :"
                                    + e2.getMessage());
                    return encode(StringUtils.leftPad((new String(b, "UTF-8")), 10, '_').getBytes(UTF8));
                }
            }
            byte[] bytes = md.digest(b);
            return encode(bytes);
        } catch (UnsupportedEncodingException e3) {
            LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
            return null;
        }
    }

    /**
     * @param password
     * @return as secure hash of the supplied password, unsuitable for keys as
     *         its too long.
     */
    public static String secureHash(String password) {
        try {
            MessageDigest md;
            try {
                md = MessageDigest.getInstance(SECURE_HASH_DIGEST);
            } catch (NoSuchAlgorithmException e) {
                try {
                    md = MessageDigest.getInstance("SHA-1");
                } catch (NoSuchAlgorithmException e1) {
                    try {
                        md = MessageDigest.getInstance("MD5");
                    } catch (NoSuchAlgorithmException e2) {
                        LOGGER.error(
                                "You have no Message Digest Algorightms intalled in this JVM, secure Hashes are not availalbe, encoding bytes :"
                                        + e2.getMessage());
                        return encode(StringUtils.leftPad(password, 10, '_').getBytes(UTF8));
                    }
                }
            }
            byte[] bytes = md.digest(password.getBytes(UTF8));
            return encode(bytes);
        } catch (UnsupportedEncodingException e3) {
            LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
            return null;
        }
    }

    /**
     * Generate an encoded array of chars using as few chars as possible
     * 
     * @param hash
     *            the hash to encode
     * @param encode
     *            a char array of encodings any length you lik but probably but
     *            the shorter it is the longer the result. Dont be dumb and use
     *            an encoding size of < 2.
     * @return
     * @deprecated use encode(byte[])
     */
    @Deprecated
    public static String encode(byte[] hash, char[] encode) {
        return encode(hash);
    }

    public static String encode(byte[] hash) {
        return Base64.encodeBase64URLSafeString(hash);
    }

    /**
     * Converts to an Immutable map, with keys that are in the filter not
     * transfered. Nested maps are also transfered.
     * 
     * @param <K> the type of the key
     * @param <V> the type of the value
     * @param source a map of values to start with
     * @param modified a map to oveeride values in source
     * @param include if not null, only include these keys in the returned map
     * @param exclude if not null, exclude these keys from the returned map
     * @return a map with the modifications applied and filtered by the includes and excludes
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> getFilterMap(Map<K, V> source, Map<K, V> modified, Set<K> include,
            Set<K> exclude, boolean includingRemoveProperties) {
        if ((modified == null || modified.size() == 0) && (include == null)
                && (exclude == null || exclude.size() == 0)) {
            if (source instanceof ImmutableMap) {
                return source;
            } else {
                return ImmutableMap.copyOf(source);
            }
        }

        Builder<K, V> filteredMap = new ImmutableMap.Builder<K, V>();
        for (Entry<K, V> e : source.entrySet()) {
            K k = e.getKey();
            if (include == null || include.contains(k)) {
                if (exclude == null || !exclude.contains(k)) {
                    if (modified != null && modified.containsKey(k)) {
                        V o = modified.get(k);
                        if (o instanceof Map) {
                            filteredMap.put(k, (V) getFilterMap((Map<K, V>) o, null, null, exclude,
                                    includingRemoveProperties));
                        } else if (includingRemoveProperties) {
                            filteredMap.put(k, o);
                        } else if (!(o instanceof RemoveProperty)) {
                            filteredMap.put(k, o);
                        }
                    } else {
                        Object o = e.getValue();
                        if (o instanceof Map) {
                            filteredMap.put(k, (V) getFilterMap((Map<K, V>) e.getValue(), null, null, exclude,
                                    includingRemoveProperties));
                        } else {
                            filteredMap.put(k, e.getValue());
                        }
                    }
                }
            }
        }
        if (modified != null) {
            // process additions
            for (Entry<K, V> e : modified.entrySet()) {
                K k = e.getKey();
                if (!source.containsKey(k)) {
                    V v = e.getValue();
                    if (!(v instanceof RemoveProperty) && v != null) {
                        filteredMap.put(k, v);
                    }
                }
            }
        }
        return filteredMap.build();
    }

    /**
     * Converts a map into Map or byte[] values with String keys. No control
     * over depth of nesting. Keys in the filter set are not transfered
     * Resulting map is mutable.
     * 
     * @param source a map of values to modify
     * @param filter a map of values to remove by key from source
     * @return the map less any keys from filter
     */
    @SuppressWarnings("unchecked")
    public static Map<String, Object> getFilteredAndEcodedMap(Map<String, Object> source, Set<String> filter) {
        Map<String, Object> filteredMap = Maps.newHashMap();
        for (Entry<String, Object> e : source.entrySet()) {
            if (!filter.contains(e.getKey())) {
                Object o = e.getValue();
                if (o instanceof Map) {
                    filteredMap.put(e.getKey(), getFilteredAndEcodedMap((Map<String, Object>) o, filter));
                } else {
                    filteredMap.put(e.getKey(), o);
                }
            }
        }
        return filteredMap;
    }

    /**
     * @return a UUID, compact encoded, suitable for use in URLs
     */
    public static String getUuid() {
        return StorageClientUtils.encode(Type1UUID.next());
    }

    /**
     * @param object
     * @return the store object as an int.
     */
    public static int toInt(Object object) {
        if (object instanceof Integer) {
            return ((Integer) object).intValue();
        } else if (object == null || object instanceof RemoveProperty) {
            return 0;
        }
        return Integer.parseInt(toString(object), ENCODING_BASE);
    }

    /**
     * @param object
     * @return the store object as a Long
     */
    public static long toLong(Object object) {
        if (object instanceof Long) {
            return ((Long) object).longValue();
        } else if (object == null || object instanceof RemoveProperty) {
            return 0;
        }
        return Long.parseLong(toString(object), ENCODING_BASE);
    }

    /**
     * @param object
     * @return the store object as a {@link Calendar}
     * @throws ParseException
     * @deprecated no need to convert, just get the calendar object directly out of the store.
     */
    @Deprecated
    public static Calendar toCalendar(Object object) throws ParseException {
        if (object instanceof Calendar) {
            return (Calendar) object;
        } else if (object == null || object instanceof RemoveProperty) {
            return null;
        }
        final SimpleDateFormat sdf = new SimpleDateFormat(ISO8601_JCR_PATTERN, Locale.ENGLISH);
        final Date date = sdf.parse(toString(object));
        final Calendar c = Calendar.getInstance();
        c.setTime(date);
        return c;
    }

    /**
     * @param path
     * @param child
     * @return create a new path by appending the child to the parent path.
     */
    public static String newPath(String path, String child) {
        if (!path.endsWith("/")) {
            if (!child.startsWith("/")) {
                return path + "/" + child;
            } else {
                return path + child;

            }
        } else {
            if (!child.startsWith("/")) {
                return path + child;
            } else {
                return path + child.substring(1);
            }
        }
    }

    /**
     * @param <T>
     * @param setting
     * @param defaultValue
     * @return gets a setting of type <T> usign the default value if null.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getSetting(Object setting, T defaultValue) {
        if (setting != null) {
            if (defaultValue.getClass().isAssignableFrom(setting.getClass())) {
                return (T) setting;
            }
            // handle conversions
            if (defaultValue instanceof Long) {
                return (T) new Long(String.valueOf(setting));
            } else if (defaultValue instanceof Integer) {
                return (T) new Integer(String.valueOf(setting));
            } else if (defaultValue instanceof Boolean) {
                return (T) new Boolean(String.valueOf(setting));
            } else if (defaultValue instanceof Double) {
                return (T) new Double(String.valueOf(setting));
            } else if (defaultValue instanceof String[]) {
                return (T) StringUtils.split(String.valueOf(setting), ',');
            }
            return (T) setting;
        }
        return defaultValue;
    }

    /**
     * @param id
     * @return perform a 3 level shard of the path using base^^2 as the width of
     *         the shard where base is the cardinality of the encoding of the
     *         ID.
     */
    // TODO: There is no reason to use this method in sparse (or very little),
    // check usage.
    // For instance the SparsePrincipal uses it.
    public static String shardPath(String id) {
        String hash = insecureHash(id);
        return hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash.substring(4, 6) + "/" + id;
    }

    /**
     * @param string
     * @return an escaped version of the supplied string suitable for using in a
     *         stored array. The implemenation is not that efficient.
     */
    // TODO: Unit test
    public static String arrayEscape(String string) {
        string = string.replaceAll("%", "%1");
        string = string.replaceAll(",", "%2");
        return string;
    }

    /**
     * @param string
     * @return reverse the arrayEscape operation.
     */
    // TODO: Unit test
    public static String arrayUnEscape(String string) {
        string = string.replaceAll("%2", ",");
        string = string.replaceAll("%1", "%");
        return string;
    }

    /**
     * @param object
     * @return null or the store object converted to a string[]
     * @deprecated no need to convert, just get the String[] object directly out of the store.
     */
    @Deprecated
    public static String[] toStringArray(Object object) {
        if (object instanceof String[]) {
            return (String[]) object;
        } else if (object == null) {
            return null;
        } else {
            String[] v = StringUtils.split(StorageClientUtils.toString(object), ',');
            for (int i = 0; i < v.length; i++) {
                v[i] = StorageClientUtils.arrayUnEscape(v[i]);
            }
            return v;
        }
    }

    /**
     * @param object
     * @return null or the store object converted to a Calendar[]
     * @throws ParseException
     * @deprecated no need to convert, just get the Calendar[] object directly out of the store.
     */
    @Deprecated
    public static Calendar[] toCalendarArray(Object object) throws ParseException {
        if (object instanceof Calendar[]) {
            return (Calendar[]) object;
        } else if (object == null) {
            return null;
        } else {
            String[] v = StringUtils.split(StorageClientUtils.toString(object), ',');
            Calendar[] c = new Calendar[v.length];
            for (int i = 0; i < v.length; i++) {
                c[i] = toCalendar(arrayUnEscape(v[i]));
            }
            return c;
        }
    }

    /**
     * @param parameterValues
     * @return ensure that a string[] is not null after conversion from store.
     *         Use this to make iterators easier on stored arrays.
     */
    // TODO: Unit test
    public static String[] nonNullStringArray(String[] parameterValues) {
        if (parameterValues == null) {
            return new String[0];
        }
        return parameterValues;
    }

    /**
     * Adapt an object to a session. I haven't used typing here becuase I don't
     * want to bind to the Jars in question and create dependencies.
     * 
     * @param source
     *            the input object that the method
     * @return
     */
    public static Session adaptToSession(Object source) {
        if (source instanceof SessionAdaptable) {
            return ((SessionAdaptable) source).getSession();
        } else {
            // assume this is a JCR session of someform, in which case there
            // should be a SparseUserManager
            Object userManager = safeMethod(source, "getUserManager", new Object[0], new Class[0]);
            if (userManager != null) {
                return (Session) safeMethod(userManager, "getSession", new Object[0], new Class[0]);
            }
            return null;
        }
    }

    /**
     * Make the method on the target object accessible and then invoke it.
     * @param target the object with the method to invoke
     * @param methodName the name of the method to invoke
     * @param args the arguments to pass to the invoked method
     * @param argsTypes the types of the arguments being passed to the method
     * @return
     */
    private static Object safeMethod(Object target, String methodName, Object[] args,
            @SuppressWarnings("rawtypes") Class[] argsTypes) {
        if (target != null) {
            try {
                Method m = target.getClass().getMethod(methodName, argsTypes);
                if (!m.isAccessible()) {
                    m.setAccessible(true);
                }
                return m.invoke(target, args);
            } catch (Throwable e) {
                LOGGER.info("Failed to invoke method " + methodName + " " + target, e);
            }
        }
        return null;
    }

    /**
     * @param property
     * @return
     * @deprecated no need to convert, just get the Boolean object directly out of the store.
     */
    @Deprecated
    public static boolean toBoolean(Object property) {
        return "true".equals(StorageClientUtils.toString(property));
    }

    /**
     * Delete an entire tree starting from the deepest part of the tree and
     * working back up. Will stop the moment a permission denied is encountered
     * either for read or for delete.
     *
     * @param contentManager
     * @param path
     * @throws AccessDeniedException
     * @throws StorageClientException
     */
    public static void deleteTree(ContentManager contentManager, String path)
            throws AccessDeniedException, StorageClientException {
        Content content = contentManager.get(path);
        if (content != null) {
            for (String childPath : content.listChildPaths()) {
                deleteTree(contentManager, childPath);
            }
        }
        contentManager.delete(path);
    }

    public static String getInternalUuid() {
        return getUuid() + "+"; // URL safe base 64 does not use + chars
    }

}