All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.eBus.util.IndexCache Maven / Gradle / Ivy

//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// 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 Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright (C) 2007, 2011, 2013. Charles W. Rapp.
// All Rights Reserved.
//

package net.sf.eBus.util;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Persists a one or more 4-byte, integer index values to a
 * specified file. This class allows an application to track an
 * index value across executions. An example of this is tracking
 * the incoming and outgoing sequence numbers used by a
 * communication protocol.
 * 

* An index cache is created by calling * {@link #open(java.lang.String) open} and passing in the cache * file name. If the index cache is currently open, then the * existing instance is returned. Otherwise, if the file either * does not exist or is of zero length, then a new index cache is * created, containing no indices. If the file exists and is of * non-zero length, the cache file is opened and its subordinate * indices are read in. The {@link #indexCount()} method returns * the number of subordinate indices in the cache. *

*

* An {@link Index} instance is retrieved by calling * {@link #getIndex(String) getIndex}. A new index is added to the * cache by calling * {@link #addIndex(String, long, long, long, boolean) addIndex}. * An index may have either a positive increment or a negative * increment. If positive, then the initial index must be ≤ to * the final index. If negative, then the initial index must by * ≥ to the final index. The increment may not be zero. An * existing index is removed from the cache via the method * {@link #removeIndex(String) removeIndex}. *

*

* The method {@link Index#nextIndex()} returns the incremented * index. If the increment index exceeds the final index (either * > or < depending on whether the increment is > or * < zero), then the "reset on final index" flag is checked to * determine what to do next. If this flag is {@code true}, then * the index is reset to the initial index value and that value * is returned. If {@code false}, then an * {@link IndexOutOfBoundsException} is thrown and the index * can no longer be used. *

*

* A long is used because that will support all other integer * types. For smaller integer types, configure the instance * accordingly. *

*

* Note: the cache file is not written only when * the index cache is closed. This class puts adds a thread to * the * {@link java.lang.Runtime#addShutdownHook(java.lang.Thread)}. * This thread closes all open index cache files when the JVM * shuts down. *

*

* This class does not support sharing the index cache * file between simultaneously executing applications. Such use * will corrupt the index file and result in undetermined * results. Neither {@code IndexCache} nor * {@code IndexCache.Index} classes are thread-safe. If there is * a need to share an {@code IndexCache} instance among threads, * then the application performs the appropriate synchronization. * The static data member {@link #open(String)} and * {@link #isCacheOpen(java.lang.String)} are thread-safe. *

*

* If you want unique, reusable index values that do not need * to be persisted between application executions, then use * {@link net.sf.eBus.util.IndexPool}. *

* * @see net.sf.eBus.util.IndexPool * * @author Charles Rapp */ public final class IndexCache { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * The default initial index is zero. */ public static final long DEFAULT_INITIAL_INDEX = 0L; /** * The default final index is {@code Long.MAX_VALUE}. */ public static final long DEFAULT_FINAL_INDEX = Long.MAX_VALUE; /** * The default index increment is one. */ public static final long DEFAULT_INCREMENT = 1L; /** * Automatic index reset is turned off by default. */ public static final boolean DEFAULT_AUTO_RESET = false; /** * An index name may be at most 20 characters long. */ public static final int MAXIMUM_INDEX_NAME_LENGTH = 20; /** * Force file contents to be asynchronously written to the * device. */ private static final String ACCESS_MODE = "rwd"; /** * An existing cache file must contain at least 4 bytes which * is the cache size. */ private static final long MINIMUM_FILE_LENGTH = 4L; /** * The first index is placed after the cache size at this * offset. */ private static final long INDEX_OFFSET = 4L; /** * Each index takes up 54 bytes: *
    *
  • * Index name: 21 bytes (1 byte length + 20 bytes name) *
  • *
  • * Initial index: 8 bytes *
  • *
  • * Final index: 8 bytes *
  • *
  • * Increment: 8 bytes *
  • *
  • * Reset flag: 1 byte *
  • *
  • * Current index: 8 bytes *
  • *
* Total: 54 bytes */ private static final long INDEX_SIZE = 54L; private static final byte PADDING_VALUE = (byte) 0; private static final byte[] NAME_PADDING = { PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE, PADDING_VALUE }; private static final String CORRUPT_INDEX_CACHE = "corrupt index cache"; //----------------------------------------------------------- // Statics. // /** * Maps the cache file name to its encapsulating index * cache instance. */ private static final Map sCacheMap = new HashMap<>(); /** * Use this lock to protected the index cache map. */ private static final Lock sCacheMutex = new ReentrantLock(); //----------------------------------------------------------- // Locals. // /** * The indices within this cache. Maps index name to the * index instance. */ private final Map mIndices; /** * The cache file name. */ private final String mCacheFileName; /** * The cache file random access instance. */ private final RandomAccessFile mCacheFile; /** * Track when any the cache was last modified. */ private final Date mLastModified; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * The index cache with its individual indices and cache * file. * @param indices the initial cache indices. May be empty. * @param cacheFileName the cache file name as supplied by * user. * @param cacheFile the random access file instance. * @param lastModified the file last modified date. */ private IndexCache(final Map indices, final String cacheFileName, final RandomAccessFile cacheFile, final Date lastModified) { mIndices = indices; mCacheFileName = cacheFileName; mCacheFile = cacheFile; mLastModified = lastModified; } // end of IndexCache(Map<>, String, RandomAccessFile, Date) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // Get methods. // /** * Returns the cache file name as originally supplied by * user. * @return the cache file name. */ public String cacheFileName() { return (mCacheFileName); } // end of cacheFileName() /** * Returns the timestamp when the cache was last modified. * This value is set to the cache file last modified date * when opened or to the current date and time when created. * @return the timestamp when the cache was last modified. */ public Date lastModified() { return (new Date(mLastModified.getTime())); } // end of lastModified() /** * Returns {@code true} if this cache has no indices and * {@code false} if it has at least one index. * @return {@code true} if this cache has no indices and * {@code false} if it has at least one index. */ public boolean isEmpty() { return (mIndices.isEmpty()); } // end of isEmpty() /** * Returns the number of indices in this cache. * @return the number of indices in this cache. */ public int indexCount() { return (mIndices.size()); } // end of indexCount() /** * Returns a list of current index names. The returned list * is a copy of the actual list and so may be modified * without impacting the actual list. * @return a list of current index names. */ public List indexNames() { return (new ArrayList<>(mIndices.keySet())); } // end of indexNames() /** * Returns {@code true} if this cache contains the named * index and {@code false} otherwise. * @param indexName the index name. * @return {@code true} if this cache contains the named * index and {@code false} otherwise. * @see #getIndex(String) * @see #addIndex(String, long, long, long, boolean) * @see #removeIndex(String) */ public boolean containsIndex(final String indexName) { return (mIndices.containsKey(indexName)); } // end of containsIndex(String) /** * Returns the named index instance. May return {@code null}. * @param indexName the index name. * @return the specified named index instance. * @see #containsIndex(String) * @see #addIndex(String, long, long, long, boolean) * @see #removeIndex(String) */ public Index getIndex(final String indexName) { return (mIndices.get(indexName)); } // end of getIndex(String) /** * Returns {@code true} if the named index cache is open and * {@code false} if it is not. * @param cacheName the index cache file name. * @return {@code true} if the named index cache is open and * {@code false} if it is not. */ public static boolean isCacheOpen(final String cacheName) { boolean retcode = false; sCacheMutex.lock(); try { retcode = sCacheMap.containsKey(cacheName); } finally { sCacheMutex.unlock(); } return (retcode); } // end of isCacheOpen(String) // // end of Get methods. //----------------------------------------------------------- /** * Returns the new index added to the cache. Note that * {@code increment} may be either > or < zero and * that {@code initialIndex} may equal {@code finalIndex}. * @param indexName the index name. Must be unique within * {@code this IndexCache}. * @param initialIndex the index initial value. * @param finalIndex the index maximum value. The index * will not exceed this value. * @param increment increment the current value by this * amount to get the next index value. The increment may * be either > zero or < zero. * @param resetFlag if {@code true}, then reset the index * to the initial value when the final value is exceeded. * @return the newly added index. * @throws IllegalArgumentException * if: *
    *
  • * if {@code indexName} is {@code null}, an empty string * or longer than {@link #MAXIMUM_INDEX_NAME_LENGTH}, *
  • *
  • * if {@code indexName} is not unique within * {@code this IndexCache} instance, *
  • *
  • * {@code increment} is zero, *
  • *
  • * {@code initialIndex} is > {@code finalIndex} and * {@code increment} is > zero or *
  • *
  • * {@code initialIndex} is < {@code finalIndex} and * {@code increment} is < zero *
  • *
*/ public Index addIndex(final String indexName, final long initialIndex, final long finalIndex, final long increment, final boolean resetFlag) { Index retval = null; if (indexName == null) { throw ( new IllegalArgumentException("null indexName")); } else if (indexName.length() == 0) { throw ( new IllegalArgumentException("empty indexName")); } else if (indexName.length() > MAXIMUM_INDEX_NAME_LENGTH) { throw ( new IllegalArgumentException( "indexName too long")); } else if (mIndices.containsKey(indexName)) { throw ( new IllegalArgumentException( "indexName not unique")); } else if (increment == 0L) { throw ( new IllegalArgumentException("zero increment")); } else if (increment > 0L && initialIndex > finalIndex) { throw ( new IllegalArgumentException( "increment > 0 and initialIndex > finalIndex")); } else if (increment < 0L && initialIndex < finalIndex) { throw ( new IllegalArgumentException( "increment < 0 and initialIndex < finalIndex")); } else { retval = new Index(indexName, initialIndex, finalIndex, increment, resetFlag, initialIndex); mIndices.put(indexName, retval); } return (retval); } // end of addIndex(long, long, long. boolean) /** * Returns {@code true} if the named index was removed and * {@code false} if the index is unknown. * @param indexName remove the named index at this position. * @return {@code true} if the named index was removed and * {@code false} if the index is unknown. */ public boolean removeIndex(final String indexName) { boolean retcode = mIndices.containsKey(indexName); if (retcode) { mIndices.remove(indexName); } return (retcode); } // end of removeIndex(String) /** * Resets all indices to their respective initial values. */ public void resetAllIndices() { mIndices.values().stream().forEach(Index::resetIndex); } // end of resetAllIndices() /** * Removes all indices from the cache leaving the cache * empty. */ public void clearIndices() { mIndices.clear(); } // end of clearIndices() /** * Closes the cache file and removes this index cache from * the map. This index cache must be re-opened before it may * be used again. * @see #open(java.lang.String) */ public void close() { doClose(); sCacheMutex.lock(); try { sCacheMap.remove(mCacheFileName); } finally { sCacheMutex.unlock(); } } // end of close() /** * Returns a textual representation of this index cache. * @return a textual representation of this index cache. */ @Override public String toString() { final StringBuilder retval = new StringBuilder(); retval.append("cache file = ").append(mCacheFileName) .append("\n indices ="); if (mIndices.isEmpty()) { retval.append(" (none)"); } else { mIndices.values().stream(). forEach( index -> retval.append('\n').append(index)); } return (retval.toString()); } // end of toString() /** * Returns the {@code IndexCache} instance associated with * {@code cacheFileName}. If the instance is already open, * that instance is returned. Otherwise the cache file's * existence is tested. If the file does not exist or is of * size zero, then returns a new, empty {@code IndexCache} * instance. Otherwise, the cache file is opened for reading * and writing, the index information is read in and the * instance is created from the data. * @param cacheFileName read in the index data from this * file. * @return the index cache instance associated with the * file name. * @exception IllegalArgumentException * if {@code cacheFileName} is either {@code null} or empty. * @exception IOException * if {@code cacheFileName} exists but contains corrupted * data. */ public static IndexCache open(final String cacheFileName) throws IOException { IndexCache retval = null; if (cacheFileName == null) { throw ( new IllegalArgumentException( "null cacheFileName")); } else if (cacheFileName.length() == 0) { throw ( new IllegalArgumentException( "empty cacheFileName")); } sCacheMutex.lock(); try { File fInfo = new File(cacheFileName); // Is there already an instance of this index cache? if (sCacheMap.containsKey(cacheFileName)) { // Yes. Return that instance. retval = sCacheMap.get(cacheFileName); } // No, the index cache is not in the map. // Does the file exist or is it an empty file? else if (!fInfo.exists() || fInfo.length() == 0L) { // No. Create a new, empty cache file. retval = newCache(cacheFileName); sCacheMap.put(cacheFileName, retval); } // Yes, the file exists. // Does it contain at leat the minimum size? else if (fInfo.length() < MINIMUM_FILE_LENGTH) { // No, this file is not valid. throw ( new IOException( String.format("%s invalid, %,d bytes", cacheFileName, MINIMUM_FILE_LENGTH))); } // Yes, the file is at least the minimum size. // Read in the cache file. else { retval = openCache(cacheFileName, fInfo); sCacheMap.put(cacheFileName, retval); } } finally { sCacheMutex.unlock(); } return (retval); } // end of open(String) /** * Returns a new, empty index cache backed by the given file. * @param fn creates this non-existent file. * @return a new, empty index cache backed by the given file. * @throws IOException * if an I/O error occurs opening the file. */ private static IndexCache newCache(final String fn) throws IOException { final RandomAccessFile fs = new RandomAccessFile(fn, ACCESS_MODE); final Map indices = new HashMap<>(); return (new IndexCache(indices, fn, fs, new Date())); } // end of newCache(File) /** * Returns the index cache stored in the file. * @param fn the cache file name. * @param fInfo the cache file. * @return a newly opened index cache. * @throws IOException * if an I/O error occurs reading in the cache file. */ private static IndexCache openCache(final String fn, final File fInfo) throws IOException { final IndexCache retval; try (RandomAccessFile fs = new RandomAccessFile(fn, ACCESS_MODE)) { final Map indices = new HashMap<>(); final int indexCount = fs.readInt(); final long fileSize = (INDEX_OFFSET + (indexCount * INDEX_SIZE)); int i; Index index; // If the index count is negative or the calculated // file size is larger than the actual file size, // then something is wrong with the file. if (indexCount < 0 || fileSize > fInfo.length()) { throw (new IOException(CORRUPT_INDEX_CACHE)); } for (i = 0; i < indexCount; ++i) { index = readIndex(fs); indices.put(index.name(), index); } retval = new IndexCache(indices, fn, fs, new Date(fInfo.lastModified())); } return (retval); } // end of openCache(File) /** * Returns the index read from the cache file at the current * file position. * @param fs the cache file. * @return index instance read from the cache file. * @throws IOException * if an I/O error occurred reading in the index. */ private static Index readIndex(final RandomAccessFile fs) throws IOException { final String indexName = readName(fs); final long initialIndex = fs.readLong(); final long finalIndex = fs.readLong(); final long increment = fs.readLong(); final boolean resetFlag = fs.readBoolean(); final long currentIndex = fs.readLong(); // Verify the parameters. if (increment == 0L || (increment > 0L && initialIndex > finalIndex) || (increment < 0L && initialIndex < finalIndex) || (increment > 0L && currentIndex > finalIndex) || (increment < 0L && currentIndex < finalIndex)) { throw (new IOException(CORRUPT_INDEX_CACHE)); } return (new Index(indexName, initialIndex, finalIndex, increment, resetFlag, currentIndex)); } // end of readIndex(RandomAccessFile) /** * Performs the actual work of closing an index cache file. * Writes all cached indices to file before closing. */ private void doClose() { try { // Write the index count. mCacheFile.seek(0L); mCacheFile.writeInt(mIndices.size()); // Have each existing index write out its current // value to the cache. for (Index index : mIndices.values()) { index.store(mCacheFile); } // Clear out the index map as it may no longer be // used. mIndices.clear(); } catch (IOException ioex) { // Ignore. } finally { try { mCacheFile.close(); } catch (IOException ioex) { // Ignore. } } } // end of doClose() /** * Returns an index name read from the random access file at * the file's current position. * @param fs read from this file. * @return the index name. * @throws IOException * if an I/O error occurred reading the index name. */ private static String readName(final RandomAccessFile fs) throws IOException { final int length = fs.read(); String retval = null; if (length <= 0 || length > MAXIMUM_INDEX_NAME_LENGTH) { throw (new IOException(CORRUPT_INDEX_CACHE)); } else { final byte[] data = new byte[MAXIMUM_INDEX_NAME_LENGTH]; // Read in the name bytes *and* the padding. // The padding will be ignored when the string is // instantiated. if (fs.read(data) < 0) { throw ( new IOException("error reading index name")); } retval = new String(data, StandardCharsets.US_ASCII); } return (retval); } // end of readName(RandomAccessFile, String) /** * Writes the index name to the file stream. Writes the * name length first, then the name and pads the remaining * to the maximum index name length. * @param name write this index name to the file * @param fs the random access file. * @throws IOException * if an I/O error occurs writing the index name to the file. */ @SuppressWarnings({"java:S3398"}) private static void writeName(final String name, final RandomAccessFile fs) throws IOException { final int length = name.length(); final int paddingLength = (MAXIMUM_INDEX_NAME_LENGTH - length); fs.write(length); fs.write(name.getBytes(StandardCharsets.US_ASCII)); // If the name is less than the maximum length, then // pad out the remaining with 0 bytes. if (paddingLength > 0) { fs.write(NAME_PADDING, 0, paddingLength); } } // end of writeName(String, RandomAccessFile) //--------------------------------------------------------------- // Inner classes. // /** * Tracks the individual index parameters and its location * within the cache file. {@code Index} instances are * accessed via {@link IndexCache#getIndex(String)}. This * index is incremented by the method {@link #nextIndex()}. * This index can be reset to its initial value by the method * {@link #resetIndex()}. */ public static final class Index { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * The index name. Must be unique within the cache. */ private final String mName; /** * The index initial value. If the reset-upon-final flag * is {@code true}, then the index is set to this value * when the final index is reached. */ private final long mInitialIndex; /** * The final index value. The index must either always * be ≤ this value if the increment is > 0 or * ≥ this value if increment is < 0. */ private final long mFinalIndex; /** * The index increment for determining the next increment * value. */ private final long mIncrement; /** * If this flag is {@code true}, then reset the index * to the initial value when the final value is passed. */ private final boolean mResetUponFinalFlag; /** * The current index value. Initialized to the initial * index. */ private long mCurrentIndex; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // private Index(final String name, final long initialIndex, final long finalIndex, final long increment, final boolean resetFlag, final long currentIndex) { mName = name; mInitialIndex = initialIndex; mFinalIndex = finalIndex; mIncrement = increment; mResetUponFinalFlag = resetFlag; mCurrentIndex = currentIndex; } // end of Index(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Get methods. // /** * Returns the index name. * @return the index name. */ public String name() { return (mName); } // end of name() /** * Returns the initial cache index. * @return initial cache index. */ public long initialIndex() { return (mInitialIndex); } // end of initialIndex() /** * Returns the increment used to obtain the next cached * index. * @return index increment. */ public long increment() { return (mIncrement); } // end of increment() /** * Returns the maximum cached index. * @return maximum cached index. */ public long finalIndex() { return (mFinalIndex); } // end of finalIndex() /** * Returns the current cached index. * @return current cached index. */ public long currentIndex() { return (mCurrentIndex); } // end of currentIndex() /** * Returns {@code true} if the cache automatically resets * to the initial index once the final index is reached. * @return {@code true} if the cache automatically resets * to the initial index. */ public boolean doesResetUponFinalIndex() { return (mResetUponFinalFlag); } // end of doesResetUponFinalIndex() // // end of Get methods. //------------------------------------------------------- /** * Returns the current index value. If the current index * value exceeds the final index value and if the * "reset upon final index" flag is {@code true}, then * the initial index value is returned. If the flag is * {@code false}, an * {@link java.lang.IndexOutOfBoundsException} is thrown. * Otherwise the current index is incremented prior to * returning but this incremented value is not * returned. * @return the current index value. * @throws IndexOutOfBoundsException * if the incremented index exceeds the final index value * and the index does not reset back to the initial * value. */ public long nextIndex() { long retval = mCurrentIndex; mCurrentIndex += mIncrement; if ((retval > mFinalIndex && mIncrement > 0L) || (retval < mFinalIndex && mIncrement < 0L)) { if (mResetUponFinalFlag) { retval = mInitialIndex; } else { throw ( new IndexOutOfBoundsException( String.format( "cannot increment beyond final index (%,d)", mFinalIndex))); } } mCurrentIndex = (retval + mIncrement); return (retval); } // end of nextIndex() /** * Sets the current index to the given value. * @param index the new current index. * @throws IllegalArgumentException * if {@code index} is not between the index initial * and final values. */ public void currentIndex(final long index) { if ((mIncrement > 0L && (index < mInitialIndex || index > mFinalIndex)) || (mIncrement < 0L && (index > mInitialIndex || index < mFinalIndex))) { throw ( new IllegalArgumentException( "index outside initial, final interval")); } else { mCurrentIndex = index; } } // end of currentIndex(long) /** * Resets the current index to the initial value and * returns the new current index. * @return the newly reset current index. */ public long resetIndex() { mCurrentIndex = mInitialIndex; return (mCurrentIndex); } // end of resetIndex() /** * Returns the textual representation of this index. * @return the textual representation of this index. */ @Override public String toString() { final String retval; try (Formatter output = new Formatter()) { output.format("[%s]%n", mName) .format(" initial index = %,d%n", mInitialIndex) .format(" final index = %,d%n", mFinalIndex) .format(" increment = %,d%n", mIncrement) .format(" current index = %,d%n", mCurrentIndex) .format("reset upon final = %b", mResetUponFinalFlag); retval = output.toString(); } return (retval); } // end of toString() /** * Stores the all index data to the file at the current * position. * @param fs Write to this random access file. * @throws IOException * if an error occurs writing this index to the file. */ private void store(final RandomAccessFile fs) throws IOException { writeName(mName, fs); fs.writeLong(mInitialIndex); fs.writeLong(mFinalIndex); fs.writeLong(mIncrement); fs.writeBoolean(mResetUponFinalFlag); fs.writeLong(mCurrentIndex); } // end of store(RandomAccessFile) } // end of class Index /** * When the JVM shuts down, close all open index caches. */ private static final class CacheShutdown extends Thread { //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // private CacheShutdown() {} // // end of Constructors. //------------------------------------------------------- @Override public void run() { final List caches = new ArrayList<>(); sCacheMutex.lock(); try { caches.addAll(sCacheMap.values()); sCacheMap.clear(); } finally { sCacheMutex.unlock(); } caches.stream().forEach(IndexCache::doClose); } // end of run() //----------------------------------------------------------- // Member data. // } // end of class CacheShutdown static { (Runtime.getRuntime()).addShutdownHook( new CacheShutdown()); } // end of static } // end of IndexCache // // CHANGE LOG // $Log: IndexCache.java,v $ // Revision 1.2 2007/02/23 13:38:32 charlesr // Corrected javadoc comments. // // Revision 1.1 2007/02/12 20:43:42 charlesr // Initial revision //




© 2015 - 2025 Weber Informatics LLC | Privacy Policy