|
|

Purpose: Teach reader how to make their own simple cache writing and reading system.
Difficulty: 6/10
Assumed Knowledge: Standard Java Structure, I Explain everything else in-depth
Classes Modified: CachePacker.java, CacheFile.java, CacheUnpacker.java
Refactored/Non-Refactored?: Either
In this tutorial I will be explaining in depth how to make a simple, yet effective system for packing multiple files into a single file. And then reading from that file to obtain the multiple files. The biggest advantage of using this for a RSPS client is that instead of having thousands of model and animation files which will bump up compressing, decompressing size and downloading time you can have a single file which contains all of files, and can be easily parsed during runtime.
Step 1: What kind of properties are my cached files going to need?
Well the first thing you need to do is figure out what kind of properties each file in your cache is going to need. The most common uses I could think of is the path the file would be located in within the cache. On top of these properties you will also need the byte[] Array which contains the file in byte format, which can be read by many different Java IO readers as well as a boolean which indicates whether or not to keep the file cached after loading, which may help to clear up RAM using during runtime(Always a priority)
Step 2: Creating a class to represent a cache file.
So know that you have decided what kind of properties you will need to hold for each file, you need to create a class which will represent each file within the cache and hold each of those properties for each file.
Here is my template for this class: (Contains the essentials for this usage of the class)
Here is my finished CacheFile class after I have adjusted it to support my properties of after loading storage and the path within the cache the file is located in.Code:import java.io.ByteArrayInputStream; public class CacheFile { private boolean store; private byte[] bytes; public CacheFile(boolean store, Arguments Here, byte[] bytes) { this.store = store; this.bytes = bytes; } public boolean getStore() { return store; } public byte[] getBytes() { return bytes; } public ByteArrayInputStream getNewInputStream() { return new ByteArrayInputStream(bytes); } }
Step 3: An overview of the cache packing system you will use.Code:import java.io.ByteArrayInputStream; public class CacheFile { private boolean store; private String path; private byte[] bytes; public CacheFile(boolean store, String path, byte[] bytes) { this.store = store; this.path = path; this.bytes = bytes; } public boolean getStore() { return store; } public String getPath() { return path; } public byte[] getBytes() { return bytes; } public ByteArrayInputStream getNewInputStream() { return new ByteArrayInputStream(bytes); } }
Ok so the way we are going to pack multiple files into a single cache is by first, getting the amount of files that are in the cache and writing that into the file. This way the cache unpacker can know how many files are in the cache without any troubles.
Then, for each file we are going to write, we ill write the following:
- ID : The id of the file being written, starting at 0 and incrementing for each file
- Any Properties : For each custom property the CacheFile class contains
- Bytes : The file in byte array form, which as stated earlier can be read by many of the Java IO readers.
Then we will simply make sure all the data is saved to the file and close it off. Done! The cache will then be in a single file.
Step 4: Tackling the structure of your cache packer
Now that you have an idea of how the cache packer is going to operate, you are going to have to create your CachePacker class(or whatever you chose to call it). Following this you will need to set up your imports and 3 static methods: One to take in a output file, a list of files to pack and a optional boolean of whether to stop overwriting, One to take in a byte array and write both its size and contents to the file and finally One to convert any file into a byte[] array.
Make sure to read the comments!
Well you are going to notice I have highlighted a few things in this template, here are the explainations:Code:import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.List; public class CachePacker { // Because it is easy to put all of this in one method, // I have done so. I have also made it a static method // so that you do not need to initiate an instance of // this class to be able to use this method. public static void createCache(String cacheFile, List<String> files, List<Boolean> stores, List<YourProperty> YourProperties, boolean... overwrite) { } // My method for writing bytes to the stream // in a way which the reader will be able to // parse into a byte[] array. private static void writeBytes(byte[] bytes, DataOutputStream out) throws IOException { out.writeShort(bytes.length); out.write(bytes); } // Simple method to read all of the bytes out of a file // then return with those bytes in an array. private static byte[] getBytes(String file) throws IOException { File f = new File(file); byte[] fileBytes = new byte[(int)f.length()]; new FileInputStream(f).read(fileBytes); return fileBytes; } }
1. "boolean... overwrite" - Some of you will be thinking "What is this, what does it do?", so to answer that if you are asking it. When you add "..." to the end of a Class type as an argument in Java; it can make the argument optional(If it is the last argument), but is designed to make it so that you can enter multiple arguments of that Class type when executing the method, then Java will automatically turn those arguments into a Class[] array of the given type. (i.e. the method "doSomething(int anArg, String... others)" can be executed via "doSomething(1, "string1", "string2", "string3", "etc.");"
2. "List<YourProperty> YourProperties" - Remember back in step 1 you decided what properties you were going to need for your cached files, and in step 2 you implemented those properties into your CacheFile class? Well now you have to implement them into your Cache Packer, this is rather simple however. For each custom property you have, add a new List argument to the createCache method (i.e. List<Integer> prices, List<String> nicknames)
3. "out.writeShort(bytes.length)" - Your probably wondering why this is highlighted, don't worry you don't have to change anything. This is simply just to give you a tip on these sorts of things if you don't already know it. When you are using bytes to store data, eventually the amount of space the bytes(even though they are small) take up will add up. So, when using bytes to store data you should always be aware of what type of variable you are using to store data in. In this example, even though "bytes.length" is an Integer, I have it being written as a short because I am not expecting the length of that array to exceed that a Short can hold. And because an Integer takes up 4 bytes, while Shorts take up two, I am saving space(Once again it adds up!) by using a Short instead of an Integer(Hope that explains that)
Ok so now that you've hopefully read through that you should have your createCache method looking something like this:
Though because my extra property is the file path, it is already given in the files List and I don't need to add any extra arguments.Code:public static void createCache(String cacheFile, List<String> files, List<Boolean> stores, List<Integer> somevalue, boolean... overwrite) { }
Step 5: Checking and opening our cache for writing
Now just some fairly standard procedures to add to our method here, the first bit:
So to just summarize on all those comments, we first simply create an instance of File giving it the location of the cacheFile argument, then proceed to check if we aren't aloud to overwrite an existing file and finally create a standard TryCatch statement which opens a new DataOutputStream to write in.Code:// Create the File class associated with the // output file for the cache. File file = new File(cacheFile); // Check if our overwrite argument has been // given a value by checking length, then // checking if the first (presumably only) // value is true. If then the file exists // a new Exception will be created, traced // and then the method will be stopped. if (overwrite.length > 0) { if (overwrite[0]) { if (file.exists()) { new Exception("File already exists").printStackTrace(); return; } } } // Standard try catch exception with the creation of a new // DataOutputStream using our previously instanced file. try { DataOutputStream out = new DataOutputStream(new FileOutputStream(file)); } catch (Exception e) { e.printStackTrace(); }
Step 5: Writing files to the DataOutputStream
Ok so by now you should have a good idea of what's going on, so I'm not going to spend much time elabourating on the comments. Add this underneath the line creating a new DataOutputStream:
You guessed it right(I hope), after reading through the comments you should have assumed that you replace the blue code with that appropriate to the properties you decided to create.Code:// Write the amount of files going to be packed (Length of File List) out.writeInt(files.size()); // Get the base directory for all of the cache files after making sure // the slashes are in sync and initialize fileID cacheFile = cacheFile.replaceAll("/", "\\"); String cacheDirectory; if (cacheFile.contains("\\")) { cacheDirectory = cacheFile.substring(0, cacheFile.lastIndexOf("\\")); } else { new Exception("CacheFolder not seperate").printStackTrace(); out.close(); return; } int fileID = 0; // For each file listed in the File List, // give it an ID, set it's name and whether // or not it should be stored. As well as // get the file contents as a byte[] and // write that. for (int i=0; i<files.size(); i++) { out.writeInt(fileID++); // Write the file id then increment it by 1 // If we have a list indicating whether to store // each file or not go by that, other wise go // off of a default value. if (stores != null) out.writeBoolean(stores.get(i)); else out.writeBoolean(false); String filePath = files.get(i); // BEGIN CUSTOM PROPERTIES writeBytes(filePath.getBytes(), out); // END CUSTOM PROPERTIES if (!cacheDirectory.endsWith("\\")) cacheDirectory += "\\"; writeBytes(getBytes(cacheDirectory + filePath), out); }
To help you out heres a small list of common variables you can write:
If you want to add more flexibility with the extra lists you added, use this template for them. This way you can set a List to null and all files will be given a default value.Code:out.writeInt(INTEGER); out.writeBoolean(BOOLEAN); out.writeShort(SHORT); out.writeLong(LONG); out.writeDouble(DOUBLE); writeBytes((STRING).getBytes(), out); // Using my method
Step 6: Flushing and Closing our cache
Now a the easiest part of all, flushing and closing our DataOutputStream. Add this just before the end of the try brackets:
And now your AlMOST THERE!!!. Now you just need to adjust my class to read your cache.Code:// And finally we flush the stream to make sure // all the data we have wrote to the stream gets // put in the file and close it so that other // applications may open the file for reading/writing. out.flush(); out.close();
Spoiler for Heres what your CachePacker class should look like:

Step 7: HashMaps
If you already know about HashMaps, skip this step.
(In my own words)
HashMaps take two Classes(i.e. CachePacker, Integer, String, CacheFile etc. e.g. HashMap<String, Integer>) and act like a bunch of locks. The first Class that it takes is the key to get the value of the second class. But you can also access a HashMap like a List using .get(INT) and .size().
Though I can't really explain it too well so here are 3 different HashMap tutorials:
[Only registered and activated users can see links. ]
[Only registered and activated users can see links. ]
[Only registered and activated users can see links. ]
Step 8: Adjusting the CacheUnpacker class
Ok so now you should know about HashMaps irregardless, in the CacheUnpacker class I've made a HashMap is used with properties <Integer, CacheFile> to store each file as its loaded in the form of our CacheFile class and with the key as the ID written in the CachePacker (See note-able places to edit down the bottom). This class is already setup for you and all you need to do is change the reading methods like you just did in the cachePacker: (Nobody gains anything from me explaining the same stuff in detail again)
Note: Remember to double check that the order you're reading variables in corresponds with your CacheFile() constructor!Code:import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.Set; public class CacheUnpacker { // Two variables needed for this class. // A HashMap to hold file information // and a boolean to state whether or not // a cache load has been called. private HashMap<Integer, CacheFile> files; private boolean loaded; public CacheUnpacker() { loaded = false; } // Attempt to load the contents of a cache created // using the same format, and parsing all files // that are included within. public void loadCache(String filePath) { files = new HashMap<Integer, CacheFile>(); loaded = true; try { // Just checking if the file exists, I've made it // manually throw an exception in case any of you // have the need to do something special if the // file doesn't exist. (i.e. Scan for similar files) File file = new File(filePath); if (!file.exists()) throw new FileNotFoundException(); // I doub't the file size is going to exceed the // Integer max, which just about 2GB DataInputStream in = new DataInputStream(new FileInputStream(file)); int packedFiles = in.readInt(); for (int i=0; i<packedFiles; i++) { // For each file, read the following and put in HashMap // Integer - The files id // Boolean - Whether or not to keep file in cache // (If a file only needs to be used once) // String via byte[] - The "file path" for the file // (If you want to get the "directory" the // file would have been in) // byte[] - The file contents in byte form files.put(in.readInt(), new CacheFile( in.readBoolean(), new String(getBytes(in)), getBytes(in))); } // Finally closing the stream as it is no longer // being used, so that other applications may // open and read/write to it. in.close(); } catch (Exception e) { e.printStackTrace(); } } public boolean isLoaded() { return loaded; } private byte[] getBytes(DataInputStream in) throws IOException { // This is the method I use to obtain the // size that is needed for a byte array // and then return one with the given data. byte[] bytes = new byte[in.readShort()]; in.read(bytes); return bytes; } }
As you will notice I marked the comments irrelevant to you in blue as well so you know how they match up. Once again heres a list of the common DataInputStream methods to replace code with:
Step 9: Once we have used the files we needed...Code:in.readInt(); in.readBoolean(); in.readShort(); in.readLong(); in.readDouble(); new String(getBytes(in)); // My Method
Remember how in step 1 I mentioned that the store valuable was required, but we haven't really done anything with it yet? Well now I'm going to explain the method used in CacheUnpacker to make use of that property.
So the way the following method works is, first makes sure a cache has been loaded, and will stop the method if one hasn't. Then it uses the HashMap getKeys() method to obtain a list of all the Key objects that have been placed in the HashMap, this way when using a cache with file id's that don't go up in increments of 1(e.g. select model cache). It then goes through that list of Key objects, checking and removing HashMap entries if they are set to be removed when this method is called(Not stored after loading) Here is the method:
Place that in your CacheUnpacker class, no editing of this method is required.Code:// Clean out HashMap once loading has // completed, ridding of any entries in // the HashMap of which are no longer // needed. public void cleanFileMap() { // If nothing has been loaded yet if (!loaded) return; // Gets all the "Key" objects that are in // the files HashMap. Doing it this way // allows there to be keys that do not // go up from 0 in increments of 1. Set<Integer> keys = files.keySet(); for (int i : keys) if (!files.get(i).getStore()) files.remove(i); }
Step 10: Obtaining loaded files from outside of the CacheUnpacker class
As you may/may not have noticed(doesn't really matter), the HashMap instance "files" in CacheUnpacker is private. So we need to make a method to get that HashMap externally, making sure that it has been initialized before we try to give it:
Once again, just add it to your CacheUnpacker class.Code:public HashMap<Integer, CacheFile> getFileMap() { // If the loadCache() method hasn't been // called, the HashMap will not have // been initialized. (Exception prevention) if (!loaded) return null; return files; }
AND VIOLA YOUR FINISHED (To an extent)
Extra: Notable places for editing
At the moment the only place you may need to edit is the fileID in the CachePacker class.
Remove this line:
Modify this line to your liking:Code:int fileID = 0;
Extra: (Advanced)(Corruption Protection) FailSafe'ing Lists that aren't SynchronizedCode:out.writeInt(fileID++);
Well theres a chance that when you make up the List's to pack your cache with that sometimes one or more wont match up to the main(File) List, so heres a simple method that will cycle all lists and unless null, return false if any of them don't match up with the File List: (Add in CachePacker)
And to implement it just add this and adjust the third++ arguments to your custom List's somewhere before the TryCatch in the createCache method of CachePacker:Code:// For flexibility of the cache packing method, // this method takes in a main List of type // <?> which enables it to be any List. Then // a Array of List<?>'s. It then loops through // the Array of Lists, and unless the Array is // null; if it's size does not match that of the // main Arrays it will return false as the List's // are not in synchronization. private static boolean isSynchronized(List<?> main, List<?>... lists) { for (List<?> l : lists) if (l != null && main.size() != l.size()) return false; return true; }
Extra: Modify CachePacker.createCache to be more flexibleCode:// Check if File List and Store List are in sync, // if they aren't throw a new exception. // HOWEVER, if you do not wish to utilize the // storage feature you may set the store list to // null and this check will be ignored. Leaving all // store variables to be set to the default true; if (!isSynchronized(files, stores, yourList1, yourList2, etc.)) { new Exception("Lists arent synchronized").printStackTrace(); return; }
Whilst in the making of a tutorial to implement this Cache system into Model Preloading it occured to me that it would be alot easier to have the createCache method accept 2 Strings first, One for where the cache file will be output and one for the input directory. So here is how to do so.
First off here:
Add this new argument:Code:public static void createCache(String cacheFile, List<String> files,
Now delete this:Code:public static void createCache(String cacheFile, String cacheDirectory, List<String> files,
Now your modified method will create the cache in the file cachePath specifies, while receiving files to place in the cache from cacheDirectory.Code:cacheFile = cacheFile.replaceAll("/", "\\"); String cacheDirectory; if (cacheFile.contains("\\")) { cacheDirectory = cacheFile.substring(0, cacheFile.lastIndexOf("\\")); } else { new Exception("CacheFolder not seperate").printStackTrace(); out.close(); return; }
Sources: (I've used this before but I can't remember it all off by heart)
[Only registered and activated users can see links. ] (To check how much 2147483647 bytes is)
[Only registered and activated users can see links. ]
FINISHED!!!! Who knows why I spent 3 hours writing this tutorial... IT BETTER HELP SOMEONE!!!
Cheers,
Ninjastylz(az)
Awesome! Repped!

Nevermind will add to second postiPlasma

so you're basically creating your own cache?

Find this tutorial elsewhere posted before me, I'll pay you if you do because this was 100% me and I spent 2-3 hours on this. I also have a dodgier version of this when I first created the idea(Surely done by someone else before though) of putting files in a single file using a byte array when I was to make 2d games using Applets.
This is the simplest form of cache creating I can think of, it just comes off as confusing until you read it because it's written in a tutorial. If you are capable of it and you just read the code(With/without comments) you would be able to understand it a whole lot more easily

Niceee, decent. Won't use though.
| « [REQ] Original PI Client [REQ] | The ultimate guide to a webclient. [Noob friendly] » |
| Thread Information |
Users Browsing this ThreadThere are currently 1 users browsing this thread. (0 members and 1 guests) |