com.flexive.ejb.beans.configuration.GenericConfigurationImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.ejb.beans.configuration.GenericConfigurationImpl.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.ejb.beans.configuration;

import com.flexive.core.Database;
import com.flexive.core.storage.StorageManager;
import com.flexive.shared.CacheAdmin;
import com.flexive.shared.EJBLookup;
import com.flexive.shared.Pair;
import com.flexive.shared.cache.FxCacheException;
import com.flexive.shared.configuration.*;
import com.flexive.shared.configuration.parameters.NullParameter;
import com.flexive.shared.configuration.parameters.ParameterFactory;
import com.flexive.shared.configuration.parameters.UnsetParameter;
import com.flexive.shared.exceptions.*;
import com.flexive.shared.interfaces.GenericConfigurationEngine;
import com.flexive.shared.interfaces.TransCacheEngine;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.CloneFailedException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Abstract base class for configuration methods. Implements templated getter/setter
 * methods for configuration classes that may add custom behavior like caching.
 * <p>
 * An implementor must create SQL statements for reading, updating and deleting
 * parameters, and a method for obtaining a database connection for the
 * configuration table.
 * </p>
 * <p>
 * The <code>setParameter/getParameter</code> methods may be overridden
 * to implement custom behavior, e.g. caching of parameter values.
 * </p>
 *
 * @author Daniel Lichtenberger (daniel.lichtenberger@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public abstract class GenericConfigurationImpl implements GenericConfigurationEngine {
    private static final Log LOG = LogFactory.getLog(GenericConfigurationImpl.class);

    /**
     * Return a (new or existing) Connection to the configuration table.
     *
     * @return a Connection to the configuration table.
     * @throws SQLException if the connection could not be retrieved
     */
    protected abstract Connection getConnection() throws SQLException;

    /**
     * Return the default scope to be used for parameters.
     *
     * @return  the default scope to be used for parameters.
     */
    protected abstract ParameterScope getDefaultScope();

    /**
     * Return a select statement that selects the given path and key
     * and returns the stored value.
     * <p>
     * Required SELECT arguments:
     * <ol>
     * <li>value</li>
     * </ol>
     * </p>
     *
     * @param conn the current connection
     * @param path the requested path
     * @param key  the requested key
     * @return a PreparedStatement selecting the value for the given path/key combination
     * @throws SQLException if a database error occurs
     */
    protected abstract PreparedStatement getSelectStatement(Connection conn, String path, String key)
            throws SQLException;

    /**
     * Return a select statement that selects all keys and values for the given path.
     * <p>
     * Required SELECT arguments:
     * <ol>
     * <li>key</li>
     * <li>value</li>
     * </ol>
     * </p>
     *
     * @param conn the current connection
     * @param path the requested path
     * @return a PreparedStatement selecting all keys and values for the given path
     * @throws SQLException if a database error occurs
     */
    protected abstract PreparedStatement getSelectStatement(Connection conn, String path) throws SQLException;

    /**
     * Return a select statement that selects all keys and values of the configuration.
     * <p>
     * Required SELECT arguments:
     * <ol>
     * <li>path</li>
     * <li>key</li>
     * <li>value</li>
     * <li>className</li>
     * </ol>
     * </p>
     *
     * @param conn the current connection
     * @return a PreparedStatement selecting all keys and values for the given path
     * @throws SQLException if a database error occurs
     */
    protected abstract PreparedStatement getSelectStatement(Connection conn) throws SQLException;

    /**
     * Return an update statement that updates the value for the given
     * path/key combination.
     *
     * @param conn  the current connection
     * @param path  path of the parameter
     * @param key   key to be updated
     * @param value the new value to be stored
     * @param className the value's class (before it was serialized to a String)
     * @return a PreparedStatement updating the given row
     * @throws SQLException        if a database error occurs
     * @throws FxNoAccessException if the caller is not permitted to update the given parameter
     */
    protected abstract PreparedStatement getUpdateStatement(Connection conn, String path, String key, String value,
            String className) throws SQLException, FxNoAccessException;

    /**
     * Return an insert statement that inserts a new row for the given
     * path, key and value.
     *
     * @param conn  the current connection
     * @param path  path of the new row
     * @param key   key of the new row
     * @param value value of the new row
     * @param className the value's class (before it was serialized to a String)
     * @return a PreparedStatement for inserting the given path/key/value
     * @throws SQLException        if a database error occurs
     * @throws FxNoAccessException if the caller is not permitted to create the given parameter
     */
    protected abstract PreparedStatement getInsertStatement(Connection conn, String path, String key, String value,
            String className) throws SQLException, FxNoAccessException;

    /**
     * Return a delete statement to delete the given parameter.
     *
     * @param conn the current connection
     * @param path path of the row to be deleted
     * @param key  key of the row to be deleted. If null, all keys under the given path should be deleted.
     * @return a PreparedStatement for deleting the given path/key
     * @throws SQLException        if a database error occurs
     * @throws FxNoAccessException if the caller is not permitted to delete the given parameter
     */
    protected abstract PreparedStatement getDeleteStatement(Connection conn, String path, String key)
            throws SQLException, FxNoAccessException;

    /**
     * Return the cache path for the given configuration parameter path.
     * If this method returns null (like the default implementation), caching
     * is disabled. Be aware that you have to add the context to your cache path,
     * e.g. the user ID for user settings.
     *
     * @param path the parameter path to be mapped
     * @return the mapped parameter path, or null to disable caching
     */
    protected String getCachePath(String path) {
        return null;
    }

    /**
     * Wrapper for simple cache stats. May be used as hook
     * for adding cache logging or as an aspectj pointcut.
     *
     * @param path the parameter path that caused the cache hit
     * @param key  the parameter key that caused the cache hit
     */
    protected void logCacheHit(String path, String key) {
        // no cache stats by default
    }

    /**
     * Wrapper for simple cache stats. May be used as hook
     * for adding cache logging or as an aspectj pointcut.
     *
     * @param path the parameter path that caused the cache hit
     * @param key  the parameter key that caused the cache hit
     */
    protected void logCacheMiss(String path, String key) {
        // no cache stats by default
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> void put(Parameter<T> parameter, String key, T value)
            throws FxApplicationException {

        if (!parameter.isValid(value)) {
            throw new FxUpdateException("ex.configuration.parameter.value", parameter, value);
        }

        // put into DB config table
        Connection conn = null;
        PreparedStatement stmt = null;
        ParameterData<T> data = parameter.getData();
        String oldValue = null;
        try {
            conn = getConnection();
            stmt = getSelectStatement(conn, data.getPath().getValue(), key);
            ResultSet rs = stmt.executeQuery();
            boolean valueExists = rs.next();
            if (valueExists) {
                oldValue = rs.getString(1);
                if (!StringUtils.isEmpty(oldValue) && oldValue.equals(String.valueOf(value)))
                    return; //no changes
            }
            stmt.close();

            try {
                writeParameter(conn, parameter, key, value, data, valueExists);
            } catch (SQLException e) {
                if (!valueExists && StorageManager.isUniqueConstraintViolation(e)) {
                    // tried to insert record, but record exists - workaround for strange
                    // bug on MySQL, where an ALTER TABLE on the configuration table messes up
                    // subsequent SELECTs (DB schema version 1353)
                    writeParameter(conn, parameter, key, value, data, true);
                } else {
                    throw new FxUpdateException(LOG, e, "ex.db.sqlError", e.getMessage());
                }
            }

            // update cache?
            String cachePath = getCachePath(data.getPath().getValue());
            if (cachePath != null) {
                putCache(cachePath, key,
                        value != null ? (Serializable) SerializationUtils.clone(value) : new NullParameter(), true);
            }
            StringBuilder sbHistory = new StringBuilder(1000);
            sbHistory.append("<parameter type=\"").append(parameter.getScope().name()).append("\">\n");
            sbHistory.append("  <path><![CDATA[").append(parameter.getPath()).append("]]></path>\n");
            sbHistory.append("  <key><![CDATA[").append(key).append("]]></key>\n");
            if (oldValue != null)
                sbHistory.append("  <oldValue><![CDATA[").append(oldValue).append("]]></oldValue>\n");
            sbHistory.append("  <value><![CDATA[").append(String.valueOf(value)).append("]]></value>\n");
            sbHistory.append("</parameter>\n");
            EJBLookup.getHistoryTrackerEngine().trackData(sbHistory.toString(), "history.parameter.set",
                    parameter.getScope().name(), parameter.getPath(), key);
        } catch (SQLException se) {
            FxUpdateException ue = new FxUpdateException(LOG, se, "ex.db.sqlError", se.getMessage());
            LOG.error(ue, se);
            throw ue;
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    private <T extends Serializable> void writeParameter(Connection conn, Parameter<T> parameter, String key,
            T value, ParameterData<T> data, boolean valueExists) throws SQLException, FxNoAccessException {
        PreparedStatement stmt = null;
        try {
            if (valueExists) {
                // update existing record
                stmt = getUpdateStatement(conn, data.getPath().getValue(), key,
                        value != null ? parameter.getDatabaseValue(value) : null,
                        value != null ? value.getClass().getName() : null);
            } else {
                // create new record
                stmt = getInsertStatement(conn, data.getPath().getValue(), key,
                        value != null ? parameter.getDatabaseValue(value) : null,
                        value != null ? value.getClass().getName() : null);
            }
            stmt.executeUpdate();
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, null, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> void put(Parameter<T> parameter, T value) throws FxApplicationException {
        put(parameter, parameter.getData().getKey(), value);
    }

    /**
     * Get a configuration parameter identified by a path and a key.
     *
     * @param parameter the actual parameter instance
     * @param path      path of the parameter
     * @param key       key of the parameter
     * @return the parameter value
     * @throws FxLoadException     if the parameter could not be loaded
     * @throws FxNotFoundException if the parameter does not exist
     */
    @SuppressWarnings({ "RedundantCast" })
    protected <T extends Serializable> Object getParameter(Parameter<T> parameter, String path, String key)
            throws FxLoadException, FxNotFoundException {
        String cachePath = getCachePath(path);
        if (parameter.isCached() && cachePath != null) {
            // try cache first
            try {
                Object value = getCache(cachePath, key);
                if (value != null) {
                    logCacheHit(path, key);
                    if (value instanceof UnsetParameter) {
                        // check for null object
                        throw new FxNotFoundException("ex.configuration.parameter.notfound", path, key);
                    } else if (value instanceof NullParameter) {
                        return null;
                    } else {
                        return value;
                    }
                }
            } catch (FxCacheException e) {
                LOG.error("Cache failure (ignored): " + e.getMessage(), e);
            }
        }
        // load parameter from config table
        logCacheMiss(path, key);
        Serializable value = loadParameterFromDb(path, key, parameter.isCached());
        if (parameter.isCached() && cachePath != null) {
            // add value to cache
            putCache(cachePath, key, (Serializable) parameter.getValue(value), false);
        }
        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> T get(Parameter<T> parameter) throws FxApplicationException {
        return get(parameter, parameter.getData().getKey());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> T get(Parameter<T> parameter, String key) throws FxApplicationException {
        return get(parameter, key, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> T get(Parameter<T> parameter, String key, boolean ignoreDefault)
            throws FxApplicationException {
        final Pair<Boolean, T> result = tryGet(parameter, key, ignoreDefault);
        if (result.getFirst()) {
            return result.getSecond();
        }
        throw new FxNotFoundException("ex.configuration.parameter.notfound", parameter.getPath(), key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> Pair<Boolean, T> tryGet(Parameter<T> parameter, String key,
            boolean ignoreDefault) {
        try {
            final T value = parameter.getValue(getParameter(parameter, parameter.getPath().getValue(), key));
            return new Pair<Boolean, T>(true, value != null ? value : parameter.getDefaultValue());
        } catch (FxNotFoundException e) {
            if (!ignoreDefault && parameter.getDefaultValue() != null) {
                return new Pair<Boolean, T>(true, parameter.getDefaultValue());
            } else {
                return new Pair<Boolean, T>(false, null);
            }
        } catch (FxApplicationException e) {
            return new Pair<Boolean, T>(false, null);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Map<ParameterData, Serializable> getAll() throws FxApplicationException {
        return getAllWithXStream(ImmutableMap.<String, XStream>of());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Map<ParameterData, Serializable> getAllWithXStream(Map<String, XStream> instances)
            throws FxApplicationException {
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = getConnection();
            stmt = getSelectStatement(conn);
            final ResultSet rs = stmt.executeQuery();
            final Map<ParameterData, Serializable> result = new HashMap<ParameterData, Serializable>();
            while (rs.next()) {
                final String key = rs.getString(2);
                final String dbValue = rs.getString(3);
                final String className = rs.getString(4);
                final ParameterPathBean path = new ParameterPathBean(rs.getString(1), getDefaultScope());

                final Parameter parameter;
                if (className != null) {
                    XStream customInstance = instances.get(className);
                    if (customInstance == null) {
                        // check prefix matches
                        String longestPrefix = "";
                        for (String name : instances.keySet()) {
                            if (name.endsWith("*") && className.startsWith(name.substring(0, name.length() - 1))
                                    && name.length() > longestPrefix.length()) {
                                customInstance = instances.get(name);
                                longestPrefix = name;
                            }
                        }
                    }

                    if (customInstance != null) {
                        // use custom XStream instance
                        parameter = ParameterFactory.newXStreamInstance(className,
                                new ParameterDataBean<Serializable>(path, key, null), customInstance);
                    } else {
                        // common case - use the parameter factory to get the matching parameter instance for the given class name
                        parameter = ParameterFactory.newInstance(className,
                                new ParameterDataBean<Serializable>(path, key, null));
                    }
                } else if ("true".equals(dbValue) || "false".equals(dbValue)) { // fallback for old configurations: try to determine the data type
                    parameter = ParameterFactory.newInstance(Boolean.class, path, key, true, null);
                } else if (StringUtils.isNumeric(dbValue)) {
                    parameter = ParameterFactory.newInstance(Integer.class, path, key, true, null);
                } else if (dbValue != null && dbValue.startsWith("<") && dbValue.endsWith(">")) {
                    // assume that it's an objects serialized to XML
                    parameter = ParameterFactory.newInstance(Object.class, path, key, true, null);
                } else {
                    parameter = ParameterFactory.newInstance(String.class, path, key, true, null);
                }
                try {
                    result.put(parameter.getData(), (Serializable) parameter.getValue(dbValue));
                } catch (XStreamException e) {
                    LOG.warn(
                            "Not including parameter " + parameter + " because its value could not be deserialized",
                            e);
                }
            }
            return result;
        } catch (SQLException e) {
            throw new FxLoadException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> Map<String, T> getAll(Parameter<T> parameter) throws FxApplicationException {
        Connection conn = null;
        PreparedStatement stmt = null;
        ParameterData<T> data = parameter.getData();
        HashMap<String, T> parameters = new HashMap<String, T>();
        try {
            conn = getConnection();
            stmt = getSelectStatement(conn, data.getPath().getValue());
            ResultSet rs = stmt.executeQuery();
            while (rs != null && rs.next()) {
                // retrieve parameters and put them in hashmap
                parameters.put(rs.getString(1), parameter.getValue(rs.getString(2)));
            }
            return parameters;
        } catch (SQLException se) {
            throw new FxLoadException(LOG, se, "ex.db.sqlError", se.getMessage());
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> Collection<String> getKeys(Parameter<T> parameter)
            throws FxApplicationException {
        Connection conn = null;
        PreparedStatement stmt = null;
        final ParameterData<T> data = parameter.getData();
        final List<String> keys = Lists.newArrayList();
        try {
            conn = getConnection();
            stmt = getSelectStatement(conn, data.getPath().getValue());
            ResultSet rs = stmt.executeQuery();
            while (rs != null && rs.next()) {
                keys.add(rs.getString(1));
            }
            return keys;
        } catch (SQLException se) {
            throw new FxLoadException(LOG, se, "ex.db.sqlError", se.getMessage());
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> void remove(Parameter<T> parameter, String key) throws FxApplicationException {
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = getConnection();
            stmt = getDeleteStatement(conn, parameter.getPath().getValue(), key);
            stmt.executeUpdate();
            String cachePath = getCachePath(parameter.getPath().getValue());
            if (cachePath != null) {
                // also remove from cache
                if (key == null) {
                    // clear entire cache path
                    deleteCache(cachePath);
                } else {
                    // clear single value
                    deleteCache(cachePath, key);
                }
            }
            if (key == null)
                EJBLookup.getHistoryTrackerEngine().track("history.parameter.remove.path",
                        parameter.getScope().name(), parameter.getPath());
            else
                EJBLookup.getHistoryTrackerEngine().track("history.parameter.remove.key",
                        parameter.getScope().name(), parameter.getPath(), key);
        } catch (SQLException e) {
            throw new FxRemoveException(LOG, e, "ex.db.sqlError", e.getMessage());
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> void remove(Parameter<T> parameter) throws FxApplicationException {
        remove(parameter, parameter.getKey());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public <T extends Serializable> void removeAll(Parameter<T> parameter) throws FxApplicationException {
        remove(parameter, null);
    }

    /**
     * Loads the given parameter from the database. Helper method for implementors.
     *
     * @param parameter the parameter to be loaded
     * @param <T>       value type of the parameter
     * @return the parameter value
     * @throws FxNotFoundException if the parameter does not exist
     * @throws FxLoadException     if the parameter could not be loaded
     */
    protected <T> T loadParameterFromDb(Parameter<T> parameter) throws FxNotFoundException, FxLoadException {
        return parameter.getValue(loadParameterFromDb(parameter.getPath().getValue(), parameter.getData().getKey(),
                parameter.isCached()));
    }

    /**
     * Loads the given parameter from the database. Helper method for implementors.
     *
     * @param path      path of the parameter
     * @param key       key of the parameter
     * @param cached    true if the cache can be used for this parameter (the value itself is not cached here,
     * but a placeholder if <strong>no</strong> value is defined).
     * @return the parameter value
     * @throws FxLoadException     if the parameter could not be loaded
     * @throws FxNotFoundException if the parameter does not exist
     */
    protected Serializable loadParameterFromDb(String path, String key, boolean cached)
            throws FxLoadException, FxNotFoundException {
        // get from DB
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = getConnection();
            stmt = getSelectStatement(conn, path, key);
            ResultSet rs = stmt.executeQuery();
            if (rs != null && rs.next()) {
                return rs.getString(1);
            } else {
                String cachePath = getCachePath(path);
                if (cachePath != null && cached) {
                    // store null object in cache to avoid hitting the DB every time
                    putCache(cachePath, key, new UnsetParameter(), false);
                }
                throw new FxNotFoundException("ex.configuration.parameter.notfound", path, key);
            }
        } catch (SQLException se) {
            throw new FxLoadException(LOG, se, "ex.db.sqlError", se.getMessage());
        } finally {
            Database.closeObjects(GenericConfigurationImpl.class, conn, stmt);
        }
    }

    /**
     * Store the given value in the cache.
     *  @param path  the parameter path
     * @param key   the parameter key
     * @param value the serializable value to be stored
     * @param fromUpdate when true, the new value was passed in by the user (i.e. it's not the "static" value stored in the DB)
     */
    protected void putCache(String path, String key, Serializable value, boolean fromUpdate) {
        try {
            if (fromUpdate || CacheAdmin.getInstance().isPathLockedInTx(path)) {
                // stay in transaction
                CacheAdmin.getInstance().put(path, key, value);
            } else {
                // we don't care about this transaction if we only cache the read-only value
                EJBLookup.getEngine(TransCacheEngine.class).putNoTx(path, key, value);
            }
        } catch (FxCacheException e) {
            LOG.warn("Failed to update cache (ignored): " + e.getMessage());
        }
    }

    /**
     * Delete the given parameter from the cache
     *
     * @param path path of the parameter to be removed
     * @param key  key  of the parameter to be removed
     */
    protected void deleteCache(String path, String key) {
        try {
            CacheAdmin.getInstance().remove(path, key);
        } catch (FxCacheException e) {
            LOG.error("Failed to update cache (ignored): " + e.getMessage());
        }
    }

    /**
     * Delete the given path from the cache
     *
     * @param path the path to be removed
     */
    protected void deleteCache(String path) {
        try {
            CacheAdmin.getInstance().remove(path);
        } catch (FxCacheException e) {
            LOG.error("Failed to update cache (ignored): " + e.getMessage());
        }
    }

    /**
     * Returns the cached value of the given parameter
     *
     * @param path the parameter path
     * @param key  the parameter key
     * @return the cached value of the given parameter
     * @throws FxCacheException if a cache exception occurred
     */
    protected Serializable getCache(String path, String key) throws FxCacheException {
        final Serializable value = (Serializable) CacheAdmin.getInstance().get(path, key);
        if (value == null || value instanceof String || Primitives.isWrapperType(value.getClass())) {
            // immutable value type
            return value;
        }
        if (value instanceof Cloneable) {
            // use clone if possible
            try {
                final Object clone = ObjectUtils.clone(value);
                if (clone != null && clone instanceof Serializable) {
                    return (Serializable) clone;
                }
            } catch (CloneFailedException e) {
                LOG.warn("Failed to clone cached configuration value " + path + "/" + key, e);
            }
        }
        // use generic clone via serialization
        return (Serializable) SerializationUtils.clone(value);
    }
}