001package Torello.JavaDoc.SyntaxHiLite;
002
003import Torello.Java.FileNode;
004import Torello.Java.StrPrint;
005import Torello.Java.FileRW;
006import Torello.Java.StorageWriter;
007
008// Needed for a JavaDoc Comment
009import Torello.Java.FileTransfer;
010
011import Torello.Java.CacheError;
012import Torello.Java.ToDoException;
013import Torello.JavaDoc.LinkJavaSource;
014
015import java.util.TreeSet;
016
017import java.io.File;
018
019/**
020 * A caching-system class that allows this tool to efficiently bypass calls to the server when
021 * an exact-copy of the hilited source-code already exists inside the cache.
022 * 
023 * <BR /><BR />
024 * <!-- <EMBED CLASS='external-html' DATA-FILE-ID=HLMC> -->
025 */
026public class HiLiteCache
027{
028    // This is, as the name clearly says, the Cache-Directory
029    // 
030    // This is now, August of 2024, officially "Package-Private"
031    // 
032    // This is to allow ripping out some of the code from this class, and inserting it into 
033    // "Helper Classes".  The code in this class has always been very difficult to read, and
034    // creating separate, smaller, helper-classes makes it a lot easier.
035    // Unfortunately, this is now "Package-Private" instead of "Private".
036
037    final String cacheSaveDirectory;
038
039    // This is the list of Hash-Codes for all Code/HTML pairs stored in the cache.  This is the
040    // exact data-structure that is referred to as the "Master Hash File"
041    // 
042    // This has also been recently converted to "Package-Private", instead of "Private"
043    // See Above Note for Details.
044
045    final TreeSet<Integer> hashCodes;
046
047    private static final short NUM_DIRS = 50;
048
049    /**
050     * Inform the user how much space (in bytes) is used by this {@code Cache}.
051     * @return The number of bytes being used on the file-system by this {@code Cache}.
052     */
053    public long totalSize()
054    {
055        return FileNode
056            .createRoot(this.cacheSaveDirectory)
057            .loadTree()
058            .getDirTotalContentsSize();
059    }
060
061    /**
062     * Count how many files and directories are contained in this {@code Cache}.
063     * @return The total number of files and sub-directories in the {@code Cache} directory.
064     */
065    public int totalNumber()
066    {
067        return FileNode
068            .createRoot(this.cacheSaveDirectory)
069            .loadTree()
070            .count();
071    }
072
073    /**
074     * This will load the hashCodes table to memory from the file-system directory identified
075     * by {@code String}-Parameter {@code 'cacheSaveDirectory'}.  An exception shall be thrown
076     * if this file is not found.
077     *
078     * @param cacheSaveDirectory This constructor presumes that this cache has been used and
079     * visited before.  This directory name should point to your local-cache of the
080     * {@code HiLite.ME} Server Code hilite past-operations.
081     *
082     * @throws CacheError This error will throw if the cache has not been instantiated, or
083     * is corrupted.  If the specified directory does not exist, then this {@code Error} shall
084     * also throw.  The chain-cause {@code Throwable} should be visible, and is included as the 
085     * {@code Throwable.getCause()}.
086     */
087    @LinkJavaSource(handle="CacheMethodsInternal", name="checkCSD")
088    @LinkJavaSource(handle="CacheMethodsInternal", name="checkTS")
089    public HiLiteCache(String cacheSaveDirectory) throws CacheError
090    {
091        this.cacheSaveDirectory = CacheMethodsInternal.checkCSD(cacheSaveDirectory);
092        this.hashCodes          = CacheMethodsInternal.checkTS(this.cacheSaveDirectory);
093    }
094
095    /**
096     * This will save the hash-code {@code TreeSet<Integer>} to disk.  The <B>Master Hash-Code
097     * List</B> just keeps a record of the hashcodes of every {@code String} that was hilited
098     * by the Hiliter <I>(and therefore saved inside the Cache).</I>  This method will save
099     * that Java {@code TreeSet} of Hash-Codes to disk.
100     *
101     * @throws CacheError This {@code Error} will throw if there is a problem writing the
102     * master cache-hash to disk.  The chain-cause {@code Throwable} should be visible, and is
103     * included as the {@code Throwable.getCause()}
104     */
105    @LinkJavaSource(handle="CacheMethodsInternal", name="persistMasterHashToDisk")
106    public void persistMasterHashToDisk() throws CacheError
107    { CacheMethodsInternal.persistMasterHashToDisk(this); }
108
109    /**
110     * Will write this method soon.  It currently is not written.
111     * 
112     * <BR /><BR /><B CLASS=JDDescLabel>TO DO:</B>
113     * 
114     * <BR />        * 
115     * This is supposed to be for "Error Recovery".  Fortunately, an error has never really
116     * happend to me, and even if it did... Just deleting the whole thing and rebuilding
117     * the Cache by running the HiLiter on all of the files seems smarter/safer anyway.
118     * This has perpetually been on the "To Do List" for 5 years now...  I think it more
119     * prudent to remind people, just delete and start over is probably smarter, it your
120     * Cache directory got messed up (for whatever reason - but mine never has anyway!)
121     */
122    public void rebuildMasterHashCache()
123    { throw new ToDoException(); }
124
125    /**
126     * This will initialize a cache-file in the file-system directory identified by parameter
127     * {@code String cacheSaveDirectory}.  If the directory specified does not exist, a
128     * {@code CacheError} is thrown.  Any old cache files will be removed.  To attempt to
129     * preserve old cache-files, call method {@code initializeOrRepair(String, StorageWriter)}
130     * 
131     * <BR /><BR /><B><I>OrClear:</I></B> If the directory structure provided to this
132     * initialize method is not empty, the <SPAN STYLE="color: red;"><B><I>its entire contents
133     * shall be erased by a call to </I></B></SPAN> (Below)
134     * 
135     * <DIV CLASS=LOC>{@code 
136     * FileTransfer.deleteFilesRecursive
137     *     (FileNode.createRoot(cacheSaveDirectory).loadTree(), sw);
138     * }</DIV>
139     * 
140     * @param cacheSaveDirectory This constructor presumes that this cache has been used and
141     * visited before.  This directory name should point to your local-cache of 
142     * {@code HiLite.ME} Server Code hilite past-operations.
143     * 
144     * @param sw This receives log-writes from the call to
145     * {@link FileTransfer#deleteFilesRecursive} which clears the files currently in the cache.
146     * This parameter may be null, and if it is, output-text will be shunted.
147     * 
148     * @throws CacheError This exception will be throw if there are errors deleting any
149     * old-cache files currently in the directory; or if there is any error creating the new
150     * master hash-cache file.  The chain-cause {@code Throwable} should be visible, and is 
151     * included as the {@code Throwable.getCause()}.
152     */
153    @LinkJavaSource(handle="CacheMethodsInternal", name="initializeOrClear")
154    public static HiLiteCache initializeOrClear(String cacheSaveDirectory, StorageWriter sw)
155        throws CacheError
156    { return CacheMethodsInternal.initializeOrClear(cacheSaveDirectory, sw); }
157
158
159    // ********************************************************************************************
160    // ********************************************************************************************
161    // Package-Private Methods
162    // ********************************************************************************************
163    // ********************************************************************************************
164
165
166    String get(
167            final String    sourceCodeAsString,
168            final String    codeTypeParam,
169            final boolean   includeLineNumbers,
170            final byte      styleNum
171        )
172    {
173        // This is how the Hash-Code is computed.  It is that simple.
174        final Integer h = Integer.valueOf(
175            codeTypeParam.hashCode() +
176            (includeLineNumbers ? 1 : 0) +
177            styleNum +
178            sourceCodeAsString.hashCode()
179        );
180
181        // NOTE: The Math.abs is OK, because it is just the directory name! (A little tricky)
182        if (! hashCodes.contains(h)) return null;
183
184        final String root = 
185            cacheSaveDirectory +
186            StrPrint.zeroPad((Math.abs(h.intValue()) % NUM_DIRS)) + 
187            File.separator + "H" + h.toString();
188
189
190        // Does a try-catch & re-throw as CacheError, with appropriate Error-Message
191        // If the file is not found - PRECISELY BECAUSE THIS WAS A CACHE-HIT => ...
192        //   ==> This would be a corrupted Cache.  The method below throws 'CacheError' if there 
193        //       are any problems reading this file.
194
195        final String saved = readFile(root + "-SOURCE.sdat");
196
197        // If the loaded Source-Code (from disk) - after a character for character comparison - is
198        // **EXACTLY EQUAL** to the Input / Requested Hi-Lite String, then the Hi-Lited Source-File
199        // is loaded from disk, and returned.
200        // 
201        // CASE 1:
202        // If there is a Cache-Hit, followed by a successful "Equals Comparison", but then the 
203        // HTML-File fails to load from disk - then (again) this must be a corrupted Cache and the
204        // method below will throw a Cache-Error if the file fails to load.
205        // 
206        // CASE 2;
207        // if the "Equals Comparison" fails - the the HTML-File is an outdated File.  This happens 
208        // if-and-when the Textual-Changes slipped by without modifying the String.hashCode() 
209        // computation.  This is why the "Equals Comparison is mandatory".  If the "equals" fails,
210        // then return null because the file will have to be re-hilited.
211
212        return saved.equals(sourceCodeAsString)
213            ? readFile(root + "-HILITE.sdat")
214            : null;
215    }
216
217    private static String readFile(final String fileName)
218    {
219        try
220            { return FileRW.readObjectFromFileNOCNFE(fileName, String.class, true); }
221
222        catch (Exception e)
223        {
224            throw new CacheError(
225                "There was an error reading a file from the cache-directory: " +
226                "    [" + fileName + "]\n" +
227                "Please see cause throwable.getCause() for more details",
228                e
229            );
230        }
231    }
232
233    void checkIn(
234            final String    sourceCodeAsString,
235            final String    hilitedCodeAsString, 
236            final String    codeTypeParam,
237            final boolean   includeLineNumbers,
238            final byte      styleNum
239        )
240    {
241        final Integer h = Integer.valueOf(
242            codeTypeParam.hashCode() +
243            (includeLineNumbers ? 1 : 0) +
244            styleNum +
245            sourceCodeAsString.hashCode()
246        );
247
248        // NOTE: The Math.abs is OK, because it is just the directory name! (A little tricky)
249        final String root = cacheSaveDirectory +
250            StrPrint.zeroPad((Math.abs(h.intValue()) % NUM_DIRS)) + File.separator;
251
252        try
253        {
254                File f = new File(root);
255                if (! f.exists()) f.mkdirs();
256        }
257
258        catch (Exception e)
259        {
260            throw new CacheError(
261                "There was an exception when accessing or creating the cache directory:\n" +
262                "    [" + root + "...sdat].\n" +
263                "See cause exception Throwable.getCause() for details.",
264                e
265            );
266        }
267
268        final String fileName = root + "H" + h.toString();
269
270        try
271        {
272            FileRW.writeObjectToFile(sourceCodeAsString,    fileName + "-SOURCE.sdat", true);
273            FileRW.writeObjectToFile(hilitedCodeAsString,   fileName + "-HILITE.sdat", true);
274        }
275
276        catch (Exception e)
277        {
278            throw new CacheError(
279                "There was an exception when writing to the cache directory:\n" +
280                "[" + root + "...sdat].\n" +
281                "Attempting to write Files:\n" + 
282                "    [" + fileName + "-SOURCE.sdat].\n" +
283                "    [" + fileName + "-HILITE.sdat].\n" +
284                "See cause exception Throwable.getCause() for details.",
285                e
286            );
287        }
288
289        hashCodes.add(h);
290        // DEBUGING System.out.println(" CHECKEDIN ");
291    }
292
293    public static void main(String[] argv)
294    {
295        if (argv.length != 1)
296        {
297            System.out.println(
298                "argv.length != 1\n" +
299                "argv[0] must contain the Cache-Directory Name.\n"
300            );
301
302            System.exit(1);
303        }
304
305        java.io.File f = new java.io.File(argv[0]);
306
307        if ((! f.exists()) || (! f.isDirectory()))
308        {
309            System.out.println(
310                "argv[0] must contain the Cache-Directory Name.\n" +
311                "Unfortunately: ((! f.exists()) || (! f.isDirectory()))\n"
312            );
313
314            System.exit(1);
315        }
316
317        initializeOrClear(argv[0], new StorageWriter());
318    }
319}