com.jkoolcloud.tnt4j.streams.utils.StreamsCache.java Source code

Java tutorial

Introduction

Here is the source code for com.jkoolcloud.tnt4j.streams.utils.StreamsCache.java

Source

/*
 * Copyright 2014-2018 JKOOL, LLC.
 *
 * 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.jkoolcloud.tnt4j.streams.utils;

import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.jkoolcloud.tnt4j.core.OpLevel;
import com.jkoolcloud.tnt4j.sink.DefaultEventSinkFactory;
import com.jkoolcloud.tnt4j.sink.EventSink;
import com.jkoolcloud.tnt4j.streams.configure.CacheProperties;
import com.jkoolcloud.tnt4j.streams.fields.ActivityInfo;

/**
 * Utility class to support TNT4J-Streams streamed data values caching.
 * <p>
 * Cache entries are defined using static or dynamic (e.g., patterns having field name variable to fill in data from
 * activity entity) values.
 * <p>
 * Streams cache supports the following configuration properties:
 * <ul>
 * <li>MaxSize - max. capacity of stream resolved values cache. Default value - {@code 100}. (Optional)</li>
 * <li>ExpireDuration - stream resolved values cache entries expiration duration in minutes. Default value - {@code 10}.
 * (Optional)</li>
 * <li>Persisted - flag indicating cache contents has to be persisted to file on close and loaded on initialization.
 * Default value - {@code false}. (Optional)</li>
 * </ul>
 *
 * @version $Revision: 3 $
 */
public final class StreamsCache {
    private static final EventSink LOGGER = DefaultEventSinkFactory.defaultEventSink(StreamsCache.class);

    private static final long DEFAULT_CACHE_MAX_SIZE = 100;
    private static final long DEFAULT_CACHE_EXPIRE_IN_MINUTES = 10;
    private static final String DEFAULT_FILE_NAME = "./persistedCache.xml"; // NON-NLS

    private static final String PARSER_NAME_VAR = "${ParserName}"; // NON-NLS

    private static Cache<String, CacheValue> valuesCache;
    private static Map<String, CacheEntry> cacheEntries = new HashMap<>(5);
    private static AtomicInteger referencesCount = new AtomicInteger();

    private static long maxSize = DEFAULT_CACHE_MAX_SIZE;
    private static long expireDuration = DEFAULT_CACHE_EXPIRE_IN_MINUTES;
    private static boolean persistenceOn = false; // TODO: file naming, because there may be running multiple concurrent
    // streams configurations (JVMs)

    private static Cache<String, CacheValue> buildCache(long cSize, long duration) {
        return CacheBuilder.newBuilder().maximumSize(cSize).expireAfterAccess(duration, TimeUnit.MINUTES).build();
    }

    /**
     * Sets cache configuration properties collection.
     *
     * @param props
     *            configuration properties to set
     *
     * @see #initialize()
     */
    public static void setProperties(Collection<Map.Entry<String, String>> props) {
        if (CollectionUtils.isNotEmpty(props)) {
            for (Map.Entry<String, String> prop : props) {
                String name = prop.getKey();
                String value = prop.getValue();
                if (CacheProperties.PROP_MAX_SIZE.equalsIgnoreCase(name)) {
                    maxSize = Long.parseLong(value);
                } else if (CacheProperties.PROP_EXPIRE_DURATION.equalsIgnoreCase(name)) {
                    expireDuration = Long.parseLong(value);
                } else if (CacheProperties.PROP_PERSISTED.equalsIgnoreCase(name)) {
                    persistenceOn = Utils.toBoolean(value);
                }
            }
        }

        initialize();
    }

    /**
     * Initializes cache setting maximum cache size and cache entries expiration duration.
     */
    public static void initialize() {
        valuesCache = buildCache(maxSize, expireDuration);

        if (persistenceOn) {
            loadPersisted();
        }
    }

    /**
     * Checks if streams cache is initialized.
     * 
     * @return {@code true} if cache is initialized, {@code false} - otherwise
     */
    public static boolean isInitialized() {
        return valuesCache != null;
    }

    /**
     * Fills in cache entries patterns with activity entity fields values and puts filled in entries to cache.
     *
     * @param ai
     *            activity entity to be used to fill in patterns data
     * @param parserName
     *            parser name
     */
    public static void cacheValues(ActivityInfo ai, String parserName) {
        if (!isInitialized()) {
            // valuesCache = buildCache(maxSize, expireDuration);
            return;
        }

        for (CacheEntry cacheEntry : cacheEntries.values()) {
            String resolvedFieldKey = fillInKeyPattern(cacheEntry.getKey(), ai, parserName);
            Object resolvedFieldValue = fillInValuePattern(cacheEntry.getValue(), ai, parserName);

            if (resolvedFieldKey != null && resolvedFieldValue != null) {
                valuesCache.put(resolvedFieldKey, new CacheValue(resolvedFieldValue, cacheEntry.isTransient()));
            }
        }
    }

    /**
     * Fills in some key pattern string with activity entity fields values.
     * 
     * @param pattern
     *            pattern string to fill
     * @param ai
     *            activity entity data
     * @param parserName
     *            parser name
     * @return pattern string filled in with data values
     */
    public static String fillInKeyPattern(String pattern, ActivityInfo ai, String parserName) {
        List<String> vars = new ArrayList<>();
        Utils.resolveCfgVariables(vars, pattern);

        return fillInPattern(pattern, vars, ai, parserName);
    }

    private static Object fillInValuePattern(String pattern, ActivityInfo ai, String parserName) {
        List<String> vars = new ArrayList<>();
        Utils.resolveCfgVariables(vars, pattern);

        return vars.size() == 1 ? ai.getFieldValue(vars.get(0)) : fillInPattern(pattern, vars, ai, parserName);
    }

    private static String fillInPattern(String pattern, List<String> vars, ActivityInfo ai, String parserName) {
        String filledInValue = pattern;

        for (String var : vars) {
            Object fieldValue;
            if (var.equals(PARSER_NAME_VAR)) {
                fieldValue = parserName;
            } else {
                fieldValue = ai.getFieldValue(var);
            }

            if (fieldValue != null) {
                filledInValue = filledInValue.replace(var, Utils.toString(fieldValue));
            }
        }

        return filledInValue;
    }

    /**
     * Resolves cache stored value identified by cache entry id.
     *
     * @param ai
     *            activity entity to be used to fill in patterns data
     * @param entryIdStr
     *            cache entity pattern identifier string
     * @param parserName
     *            parser name
     * @return resolved cached value or {@code null} if there is no such entry or data in cache
     */
    public static Object getValue(ActivityInfo ai, String entryIdStr, String parserName) {
        CacheEntry cacheEntry = cacheEntries.get(entryIdStr);
        if (cacheEntry != null) {
            String cacheKey = fillInKeyPattern(cacheEntry.getKey(), ai, parserName);
            if (cacheKey != null) {
                CacheValue value = valuesCache == null ? null : valuesCache.getIfPresent(cacheKey);
                return value == null ? null : value.value();
            } else {
                return cacheEntry.getDefaultValue();
            }
        }
        return null;
    }

    /**
     * Resolves cache stored value identified by cache entry key.
     *
     * @param cacheKey
     *            cache entry key
     * @return resolved cached value or {@code null} if there is no such entry or data in cache
     */
    public static Object getValue(String cacheKey) {
        CacheValue value = valuesCache == null ? null : valuesCache.getIfPresent(cacheKey);
        if (value == null) {
            CacheEntry cacheEntry = cacheEntries.get(cacheKey);

            return cacheEntry == null ? null : cacheEntry.getDefaultValue();
        }
        return value.value();
    }

    /**
     * Cleans cache contents.
     *
     * @see #unreferStream()
     */
    public static void cleanup() {
        if (valuesCache != null) {
            if (persistenceOn) {
                persist(valuesCache.asMap());
            }
            valuesCache.invalidateAll();
        }
        cacheEntries.clear();
    }

    /**
     * Cleans cache stored values.
     */
    public static void clearValues() {
        if (valuesCache != null) {
            valuesCache.invalidateAll();
        }
    }

    /**
     * Adds stream-cache reference.
     */
    public static void referStream() {
        referencesCount.getAndIncrement();
    }

    /**
     * Removes stream-cache reference. When last stream is unreferenced, cache gets cleaned.
     *
     * @see #cleanup()
     */
    public static void unreferStream() {
        int crc = referencesCount.decrementAndGet();

        if (crc <= 0) {
            cleanup();
            referencesCount.set(0);
        }
    }

    /**
     * Adds cache entry pattern definition to cache entry patterns map.
     *
     * @param entryId
     *            entry identifier
     * @param key
     *            entry key pattern
     * @param value
     *            entry value pattern
     * @param defaultValue
     *            default entry value
     * @return previous cache entry instance stored
     */
    public static CacheEntry addEntry(String entryId, String key, String value, String defaultValue) {
        return cacheEntries.put(entryId, new CacheEntry(entryId, key, value, defaultValue));
    }

    /**
     * Adds cache entry pattern definition to cache entry patterns map.
     *
     * @param entryId
     *            entry identifier
     * @param key
     *            entry key pattern
     * @param value
     *            entry value pattern
     * @param defaultValue
     *            default entry value
     * @param transientEntry
     *            indicating whether cache entry is transient and should not be persisted
     * @return previous cache entry instance stored
     */
    public static CacheEntry addEntry(String entryId, String key, String value, String defaultValue,
            boolean transientEntry) {
        return cacheEntries.put(entryId, new CacheEntry(entryId, key, value, defaultValue, transientEntry));
    }

    private static void loadPersisted() {
        try {
            JAXBContext jc = JAXBContext.newInstance(CacheRoot.class);
            Unmarshaller unmarshaller = jc.createUnmarshaller();
            File persistedFile = new File(DEFAULT_FILE_NAME);
            LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.loading.file", persistedFile.getAbsolutePath());
            if (!persistedFile.exists()) {
                LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                        "StreamsCache.loading.file.not.found");
                return;
            }

            CacheRoot root = (CacheRoot) unmarshaller.unmarshal(persistedFile);

            Map<String, CacheValue> mapProperty = root.getEntriesMap();
            if (MapUtils.isNotEmpty(mapProperty)) {
                for (Map.Entry<String, CacheValue> entry : mapProperty.entrySet()) {
                    valuesCache.put(entry.getKey(), entry.getValue());
                }
            }
            LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.loading.done", mapProperty == null ? 0 : mapProperty.size(),
                    persistedFile.getAbsolutePath());
        } catch (JAXBException exc) {
            Utils.logThrowable(LOGGER, OpLevel.ERROR,
                    StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.loading.failed", exc);
        }
    }

    private static void persist(Map<String, CacheValue> cacheEntries) {
        try {
            JAXBContext jc = JAXBContext.newInstance(CacheRoot.class);
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            CacheRoot root = new CacheRoot();
            root.setEntriesMap(cacheEntries);
            File persistedFile = new File(DEFAULT_FILE_NAME);
            LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.persisting.file", persistedFile.getAbsolutePath());
            marshaller.marshal(root, persistedFile);
            LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.persisting.done", cacheEntries.size(), persistedFile.getAbsolutePath());
        } catch (JAXBException exc) {
            Utils.logThrowable(LOGGER, OpLevel.ERROR,
                    StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                    "StreamsCache.persisting.failed", exc);
        }
    }

    /**
     * Adds cache entry for defined key and value.
     * 
     * @param key
     *            cache entry key
     * @param value
     *            cache entry value
     */
    public static void addValue(String key, Object value) {
        if (!isInitialized()) {
            initialize();
        }

        valuesCache.put(key, new CacheValue(value, true));
    }

    /**
     * Defines cache entry pattern.
     */
    public static class CacheEntry {
        private String id;
        private String key;
        private String value;
        private String defaultValue;
        private boolean transientEntry = false;

        /**
         * Constructs new CacheEntry.
         *
         * @param id
         *            cache entry identifier
         * @param key
         *            cache entry key pattern
         * @param value
         *            cache entry value pattern
         * @param defaultValue
         *            cache entry default value
         */
        private CacheEntry(String id, String key, String value, String defaultValue) {
            this(id, key, value, defaultValue, false);
        }

        /**
         * Constructs new CacheEntry.
         *
         * @param id
         *            cache entry identifier
         * @param key
         *            cache entry key pattern
         * @param value
         *            cache entry value pattern
         * @param defaultValue
         *            cache entry default value
         * @param transientEntry
         *            indicating whether cache entry is transient and should not be persisted
         */
        private CacheEntry(String id, String key, String value, String defaultValue, boolean transientEntry) {
            this.id = id;
            this.key = key;
            this.value = value;
            this.defaultValue = defaultValue;
            this.transientEntry = transientEntry;
        }

        /**
         * Returns cache entry identifier.
         *
         * @return entry identifier
         */
        public String getId() {
            return id;
        }

        /**
         * Returns cache entry key pattern.
         *
         * @return cache entry key pattern
         */
        public String getKey() {
            return key;
        }

        /**
         * Returns cache entry value pattern.
         *
         * @return cache entry value pattern
         */
        public String getValue() {
            return value;
        }

        /**
         * Returns default cache entry value.
         *
         * @return default cache entry value
         */
        public String getDefaultValue() {
            return defaultValue;
        }

        /**
         * Returns flag indicating that all cache entries build by this entry pattern are transient and should not be
         * persisted.
         * 
         * @return {@code true} if cache entries build by this entry pattern are transient and should not be persisted,
         *         {@code false} - otherwise
         */
        public boolean isTransient() {
            return transientEntry;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("CacheEntry{"); // NON-NLS
            sb.append("id="); // NON-NLS
            Utils.quote(id, sb);
            sb.append(", key="); // NON-NLS
            Utils.quote(key, sb);
            sb.append(", value="); // NON-NLS
            Utils.quote(value, sb);
            sb.append(", defaultValue="); // NON-NLS
            Utils.quote(defaultValue, sb);
            sb.append(", transient="); // NON-NLS
            Utils.quote(transientEntry, sb);
            sb.append('}'); // NON-NLS
            return sb.toString();
        }
    }

    /**
     * Defines cache entry value.
     */
    public static class CacheValue {
        private Object value;
        private boolean transientValue = false;

        public CacheValue(Object value) {
            this(value, false);
        }

        public CacheValue(Object value, boolean transientValue) {
            this.value = value;
            this.transientValue = transientValue;
        }

        public Object value() {
            return value;
        }

        public boolean isTransient() {
            return transientValue;
        }
    }

    /**
     * Cache entries map JAXB marshaling adapter.
     */
    public static class MapAdapter extends XmlAdapter<MapEntry[], Map<String, CacheValue>> {
        @Override
        public MapEntry[] marshal(Map<String, CacheValue> cache) throws Exception {
            List<MapEntry> pEntries = new ArrayList<>(cache.size());
            for (Map.Entry<String, CacheValue> entry : cache.entrySet()) {
                if (!entry.getValue().isTransient()) {
                    pEntries.add(new MapEntry(entry.getKey(), entry.getValue().value()));

                    LOGGER.log(OpLevel.TRACE, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                            "StreamsCache.entry.marshal", entry.getKey(), Utils.toString(entry.getValue().value()));
                } else {
                    LOGGER.log(OpLevel.TRACE, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                            "StreamsCache.entry.marshal.skip", entry.getKey(),
                            Utils.toString(entry.getValue().value()));
                }
            }

            MapEntry[] mapElements = new MapEntry[pEntries.size()];
            mapElements = pEntries.toArray(mapElements);
            return mapElements;
        }

        @Override
        public Map<String, CacheValue> unmarshal(MapEntry[] mapElements) throws Exception {
            Map<String, CacheValue> r = new ConcurrentHashMap<>(mapElements.length);
            for (MapEntry mapElement : mapElements) {
                r.put(mapElement.key, new CacheValue(mapElement.getValue()));
                LOGGER.log(OpLevel.TRACE, StreamsResources.getBundle(StreamsResources.RESOURCE_BUNDLE_NAME),
                        "StreamsCache.entry.unmarshal", mapElement.key, Utils.toString(mapElement.getValue()));
            }
            return r;
        }
    }

    /**
     * Cache map entry marshaling entity.
     */
    public static class MapEntry {
        @XmlElement
        public String key;
        private Object value;

        @XmlElement
        public Object getValue() {
            return value;
        }

        public void setValue(Object value) {
            if (value instanceof XMLGregorianCalendar) {
                this.value = ((XMLGregorianCalendar) value).toGregorianCalendar().getTime();
            } else {
                this.value = value;
            }
        }

        @SuppressWarnings("unused")
        private MapEntry() {
        } // Required by JAXB

        public MapEntry(String key, Object value) {
            this.key = key;
            setValue(value);
        }
    }

    /**
     * Root element for JAXB cache entries persisting.
     */
    @XmlRootElement
    public static class CacheRoot {

        private Map<String, CacheValue> entriesMap;

        @XmlJavaTypeAdapter(MapAdapter.class)
        public Map<String, CacheValue> getEntriesMap() {
            return entriesMap;
        }

        public void setEntriesMap(Map<String, CacheValue> map) {
            this.entriesMap = map;
        }
    }

    // private static class ByteArrayAdapter extends XmlAdapter<String, Byte[]> {
    //
    // @Override
    // public Byte[] unmarshal(String v) throws Exception {
    // return ArrayUtils.toObject(v.getBytes());
    // }
    //
    // @Override
    // public String marshal(Byte[] v) throws Exception {
    // return String.valueOf(v);
    // }
    // }

}