package org.expeditee.gui; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; import org.expeditee.io.Conversion; import org.expeditee.io.ExpReader; import org.expeditee.io.ExpWriter; import org.expeditee.io.FrameReader; import org.expeditee.io.FrameWriter; import org.expeditee.io.KMSReader; import org.expeditee.io.KMSWriter; import org.expeditee.io.Logger; import org.expeditee.items.Item; import org.expeditee.items.ItemUtils; import org.expeditee.items.Text; import org.expeditee.stats.SessionStats; /** * This class provides static methods for all saving and loading of Frames * to\from disk. This class also handles any caching of previously loaded * Frames. * * @author jdm18 * */ public class FrameIO { public static void changeParentFolder(String newFolder) { PARENT_FOLDER = newFolder; FRAME_PATH = PARENT_FOLDER + "framesets" + File.separator; IMAGES_PATH = PARENT_FOLDER + "images" + File.separator; HELP_PATH = PARENT_FOLDER + "help" + File.separator; PROFILE_PATH = PARENT_FOLDER + "profiles" + File.separator; EXPORTS_DIR = PARENT_FOLDER + "exports" + File.separator; STATISTICS_DIR = PARENT_FOLDER + "statistics" + File.separator; LOGS_DIR = PARENT_FOLDER + "logs" + File.separator; } /** * The default location for storing the framesets. Each frameset has its own * subdirectory in this directory. */ public static String PARENT_FOLDER; public static String FRAME_PATH; public static String IMAGES_PATH; public static String HELP_PATH; public static String PROFILE_PATH; public static String EXPORTS_DIR; public static String STATISTICS_DIR; public static String LOGS_DIR; private static final String INF_FILENAME = "frame.inf"; public static final String ILLEGAL_CHARS = ";:\\/?"; public static final int MAX_NAME_LENGTH = 64; public static final int MAX_CACHE = 20; private static HashMap _Cache = new HashMap(); // private static HashMap _FramesetNameCache = new // HashMap(); private static boolean ENABLE_CACHE = true; private static boolean _UseCache = true; private static boolean _SuspendedCache = false; // All methods are static, this should not be instantiated private FrameIO() { } public static boolean isCacheOn() { return _UseCache && ENABLE_CACHE; } public static void Precache(String framename) { // if the cache is turned off, do nothing if (!isCacheOn()) return; // if the frame is already in the cache, do nothing if (_Cache.containsKey(framename.toLowerCase())) return; // otherwise, load the frame and put it in the cache Logger.Log(Logger.SYSTEM, Logger.LOAD, "Precaching " + framename + "."); // do not display errors encountered to the user // (they will be shown at load time) FrameGraphics.SupressErrors(true); // loading automatically caches the frame is caching is turned on LoadUnknownPath(framename); FrameGraphics.SupressErrors(false); } /** * Checks if a string is a representation of a positive integer. * * @param s * @return true if s is a positive integer */ public static boolean isPositiveInteger(String s) { if (s == null || s.length() == 0) return false; for (int i = 0; i < s.length(); i++) { if (!Character.isDigit(s.charAt(i))) return false; } return true; } public static Frame LoadFrame(String frameName) { // If the frame name is a positive integer add the frameset name if (isPositiveInteger(frameName)){ assert(false); frameName = DisplayIO.getCurrentFrame().getFramesetNameAdjusted() + frameName; } // first try reading from cache if (isCacheOn() && _Cache.containsKey(frameName.toLowerCase())) { Logger.Log(Logger.SYSTEM, Logger.LOAD, "Loading " + frameName + " from cache."); return _Cache.get(frameName.toLowerCase()); } Logger.Log(Logger.SYSTEM, Logger.LOAD, "Loading " + frameName + " from disk."); return LoadUnknownPath(frameName); } private static Frame LoadUnknownPath(String framename) { Frame loaded = null; for (String path : UserSettings.FrameDirs) { loaded = LoadFrame(path, framename); if (loaded != null) { FrameUtils.Parse(loaded, true); break; } } return loaded; } /** * Gets the full path and file name of the frame. * * @param path- * the directory in which to look for the frameset containing the * frame. * @param frameName- * the name of the frame for which the path is being requested. * @return null if the frame can not be located. */ public static String getFrameFullPathName(String path, String frameName) { String source = path + Conversion.getFrameset(frameName) + File.separator; File tester = new File(source); if (!tester.exists()) return null; // check for the new file name format String fullPath = source + Conversion.getFrameNumber(frameName) + ExpReader.EXTENTION; tester = new File(fullPath); if (tester.exists()) return fullPath; // check for oldfile name format fullPath = source + Conversion.getFrameset(frameName) + "." + Conversion.getFrameNumber(frameName); tester = new File(fullPath); if (tester.exists()) return fullPath; return null; } public static boolean DoesFrameExist(String frameName) { for (String path : UserSettings.FrameDirs) { if (getFrameFullPathName(path, frameName) != null) return true; } return false; } private static Frame LoadFrame(String path, String frameName) { String fullPath = getFrameFullPathName(path, frameName); if (fullPath == null) return null; try { FrameReader reader; if (fullPath.endsWith(ExpReader.EXTENTION)) { reader = new ExpReader(frameName); } else { reader = new KMSReader(); } Frame frame = reader.readFrame(fullPath); if (frame == null) { FrameGraphics.ErrorMessage("Error: " + frameName + " could not be successfully loaded."); return null; } frame.path = path; // do not put 0 frames or virtual frames into the cache if (_Cache.size() > MAX_CACHE) _Cache.clear(); if (frame.getFrameNumber() > 0 && isCacheOn()) _Cache.put(frameName.toLowerCase(), frame); return frame; } catch (IOException ioe) { ioe.printStackTrace(); Logger.Log(ioe); } catch (Exception e) { e.printStackTrace(); Logger.Log(e); FrameGraphics.ErrorMessage("Error: " + frameName + " could not be successfully loaded."); } return null; } public static void Reload() { // disable cache boolean cache = _UseCache; _UseCache = false; Frame fresh = FrameIO.LoadFrame(DisplayIO.getCurrentFrame() .getFrameName()); _UseCache = cache; if (_Cache.containsKey(fresh.getFrameName().toLowerCase())) _Cache.put(fresh.getFrameName().toLowerCase(), fresh); DisplayIO.setCurrentFrame(fresh); } public static Frame LoadPrevious(Frame current) { // the current name and number String name = current.getFramesetNameAdjusted(); int num = current.getFrameNumber() - 1; // loop until a frame that exists is found for (; num >= 0; num--) { Frame f = LoadFrame(name + num); if (f != null) return f; } // if we did not find another Frame then this one must be the last one // in the frameset FrameGraphics.DisplayMessage("This is the first frame in the frameset"); return null; } /** * Returns the next Frame in the current Frameset (The Frame with the next * highest Frame number) If the current Frame is the last one in the * Frameset, or an error occurs then null is returned. * * @return The Frame after this one in the current frameset, or null */ public static Frame LoadNext(Frame current) { // the current name and number int num = current.getFrameNumber() + 1; int max = num + 1; String name = current.getFramesetNameAdjusted(); // read the maximum from the INF file try { max = ReadINF(current.path, current.getFramesetName()); } catch (IOException ioe) { FrameGraphics.ErrorMessage("Error loading INF file for frameset '" + name + "'"); return null; } // loop until a frame that exists is found for (; num <= max; num++) { Frame f = LoadFrame(name + num); if (f != null) return f; } // if we did not find another Frame then this one must be the last one // in the frameset FrameGraphics.DisplayMessage("This is the last frame in the frameset"); return null; } public static Frame LoadLast(String framesetName, String path) { // read the maximum from the INF file int max; try { max = ReadINF(path, framesetName); } catch (IOException ioe) { FrameGraphics.ErrorMessage("Error loading INF file for frameset '" + framesetName + "'"); return null; } String adjustedName = Frame.GetFramesetNameAdjusted(framesetName); // loop backwards until a frame that exists is found for (int num = max; num > 0; num--) { Frame f = LoadFrame(adjustedName + num); if (f != null) return f; } // if we did not find another Frame then this one must be the last one // in the frameset FrameGraphics.DisplayMessage("This is the last frame in the frameset"); return null; } public static Frame LoadLast() { Frame current = DisplayIO.getCurrentFrame(); return LoadLast(current.getFramesetName(), current.path); } public static Frame LoadNext() { return LoadNext(DisplayIO.getCurrentFrame()); } public static Frame LoadPrevious() { return LoadPrevious(DisplayIO.getCurrentFrame()); } /** * Deletes the given Frame on disk and removes the cached Frame if there is * one. Also adds the deleted frame into the deletedFrames frameset. * * @param toDelete * The Frame to be deleted * @return The result of File.delete() */ public static boolean DeleteFrame(Frame toDelete) throws IOException { SaveFrame(toDelete); // get the last used frame in the destination frameset final String DELETED_FRAMES = "DeletedFrames"; int lastNumber = FrameIO.getLastNumber(DELETED_FRAMES); String framePath; if (lastNumber < 0) { // create the new frameset Frame one = FrameIO.CreateFrameset(DELETED_FRAMES, toDelete.path); framePath = one.path; lastNumber = 0; } else { Frame zero = FrameIO.LoadFrame(DELETED_FRAMES + "0"); framePath = zero.path; } // get the fill path to determine which file version it is String source = getFrameFullPathName(toDelete.path, toDelete .getFrameName()); String oldFrameName = toDelete.getFrameName().toLowerCase(); // Now save the frame in the new location toDelete.setFrameset(DELETED_FRAMES); toDelete.setFrameNumber(lastNumber + 1); toDelete.path = framePath; FrameIO.SaveFrame(toDelete); if (_Cache.containsKey(oldFrameName)) _Cache.remove(oldFrameName); File del = new File(source); java.io.FileInputStream ff = new java.io.FileInputStream(del); ff.close(); return del.delete(); } /** * Creates a new Frame in the given frameset and assigns it the given Title, * which can be null. The newly created Frame is a copy of the frameset's .0 * file with the number updated based on the last recorded Frame name in the * frameset's INF file. * * @param frameset * The frameset to create the new Frame in * @param frameTitle * The title to assign to the newly created Frame (can be NULL). * @return The newly created Frame. */ public static Frame CreateFrame(String frameset, String frameTitle, String templateFrame) { int next = -1; // disable caching of 0 frames SuspendCache(); String adjustFramesetName = Frame.GetFramesetNameAdjusted(frameset); Frame template = null; if (templateFrame == null) // load in frame.0 template = LoadFrame(adjustFramesetName + "0"); else template = LoadFrame(templateFrame); ResumeCache(); // read the next number from the INF file try { next = ReadINF(template.path, frameset); } catch (IOException ioe) { ioe.printStackTrace(); Logger.Log(ioe); return null; } // set the number and title of the new frame template.setFrameNumber(++next); template.setTitle(frameTitle); Logger.Log(Logger.SYSTEM, Logger.TDFC, "Creating new frame: " + template.getFrameName() + " from TDFC"); // update INF file try { WriteINF(template.path, frameset, adjustFramesetName + next); } catch (IOException ioe) { ioe.printStackTrace(); Logger.Log(ioe); } template.setOwner(UserSettings.Username); template.resetDateCreated(); for (Item i : template.getItems()) { if (ItemUtils.isTag(i, ItemUtils.TAG_PARENT)) i.setLink(null); } return template; } public static void DisableCache() { _UseCache = false; } public static void EnableCache() { _UseCache = true; } public static void SuspendCache() { if (_UseCache) { DisableCache(); _SuspendedCache = true; } else { _SuspendedCache = false; } } public static void ResumeCache() { if (_SuspendedCache) { EnableCache(); _SuspendedCache = false; } } /** * If the userName ends in a letter a period is appended before adding the * numeric extension * * @param framesetName * the current userName * @return the exension for the current user */ public static String getFramesetExtension(String framesetName) { if (framesetName.charAt(framesetName.length() - 1) >= '0' && framesetName.charAt(framesetName.length() - 1) <= '9') { return ".1"; } return "1"; } /** * Creates a new frameset using the given name. This includes creating a new * subdirectory in the FRAME_PATH directory, Copying over the * default.0 frame from the default frameset, copying the .0 Frame to make a * .1 Frame, and creating the frameset's INF file. * * @param frameset * The name of the Frameset to create * @return The first Frame of the new Frameset (Frame.1) */ public static Frame CreateFrameset(String frameset, String path) { String conversion = frameset + " --> "; // ensure the framename is valid frameset = NameValidation(frameset); if (frameset == null) return null; conversion += frameset; Logger.Log(Logger.SYSTEM, Logger.NEW_FRAMESET, "Frameset Name: " + conversion); conversion = frameset; /** * TODO: Update this to exclude any\all invalid filename characters */ // ignore annotation character if (frameset.startsWith("@")) frameset = frameset.substring(1); conversion += " --> " + frameset; Logger.Log(Logger.SYSTEM, Logger.NEW_FRAMESET, "Name: " + conversion); // create the new Frameset directory File dir = new File(path + frameset.toLowerCase() + File.separator); dir.mkdirs(); // create the new INF file try { WriteINF(path, frameset, frameset + getFramesetExtension(frameset)); } catch (IOException ioe) { ioe.printStackTrace(); Logger.Log(ioe); } SuspendCache(); // copy the default .0 and .1 files Frame base; try { base = LoadFrame(UserSettings.DefaultFrame); } catch (Exception e) { base = new Frame(); } ResumeCache(); base.resetDateCreated(); base.setFrameset(frameset); base.setFrameNumber(0); base.path = path; SaveFrame(base, false); base.resetDateCreated(); base.setFrameNumber(1); base.setTitle(frameset); SaveFrame(base, true); Logger.Log(Logger.SYSTEM, Logger.NEW_FRAMESET, "Created new frameset: " + frameset); return base; } /** * Tests if the given String is a 'proper' framename, that is, the String * must begin with a character, end with a number with 0 or more letters and * numbers in between. If there is a dot in the framename all the chars * after it must be digits. * * @param frameName * The String to test for validity as a frame name * @return True if the given framename is proper, false otherwise. */ public static boolean isValidFrameName(String frameName) { if (frameName == null || frameName.length() < 2) return false; // String must begin with a letter and end with a digit if (!Character.isLetter(frameName.charAt(0)) || !Character.isDigit(frameName.charAt(frameName.length() - 1))) return false; int dotIndex = frameName.indexOf("."); // All the characters inbetween first and last must be letters // or digits for (int i = 1; i < frameName.length(); i++) { if (dotIndex > 0 && i > dotIndex) { if (!Character.isDigit(frameName.charAt(i))) return false; } else if (i != dotIndex) { if (!Character.isLetterOrDigit(frameName.charAt(i))) return false; } } return true; } /** * Saves the given Frame to disk in the corresponding frameset directory. * This is the same as calling SaveFrame(toSave, true) * * @param toSave * The Frame to save to disk in KMS format. */ public static String SaveFrame(Frame toSave) { return SaveFrame(toSave, true); } /** * Saves the given Frame to disk in the corresponding frameset directory, if * inc is true then the saved frames counter is incremented, otherwise it is * untouched. * * @param toSave * The Frame to save to disk in KMS format. * @param inc * True if the saved frames counter should be incremented, false * otherwise. */ public static String SaveFrame(Frame toSave, boolean inc) { if (toSave == null) return ""; // Get the full path only to determine which format to use for saving // the frame // At this stage use Exp format for saving Exp frames only. // Later this will be changed so that KMS frames will be updated to the // Exp format. String fullPath = getFrameFullPathName(toSave.path, toSave .getFrameName()); FrameWriter writer = null; try { // if its a new frame or an existing Exp frame... if (fullPath == null || fullPath.endsWith(ExpReader.EXTENTION)) { writer = new ExpWriter(); } else { writer = new KMSWriter(); } if (toSave.getFrameNumber() > 0 && ItemUtils.ContainsTag(toSave.getItems(), ItemUtils.TAG_BACKUP)) { SuspendCache(); Frame original = LoadFrame(toSave.getFrameName()); if (original == null) original = toSave; int orignum = original.getFrameNumber(); int nextnum = ReadINF(toSave.path, toSave.getFramesetName()) + 1; original.setFrameNumber(-nextnum); SaveFrame(original, inc); Item i = ItemUtils.FindTag(toSave.getItems(), ItemUtils.TAG_BACKUP); i.setLink(original.getFrameName()); toSave.setFrameNumber(orignum); ResumeCache(); } else if (toSave.getFrameNumber() < 0) { toSave.setFrameNumber(-toSave.getFrameNumber()); } toSave.setLastModifyDate(Logger.EasyDateFormat("ddMMMyyyy:HHmm")); toSave.setLastModifyUser(UserSettings.Username); writer.writeFrame(toSave); toSave.setSaved(); if (inc) SessionStats.SavedFrame(toSave.getFrameName()); // avoid out-of-sync frames (when in TwinFrames mode) if (_Cache.containsKey(toSave.getFrameName().toLowerCase())) _Cache.put(toSave.getFrameName().toLowerCase(), toSave); Logger.Log(Logger.SYSTEM, Logger.SAVE, "Saving " + toSave.getFrameName() + " to disk."); // check that the INF file is not out of date int last = ReadINF(toSave.path, toSave.getFramesetName()); if (last <= toSave.getFrameNumber()) WriteINF(toSave.path, toSave.getFramesetName(), toSave .getFrameName()); // check if this was the profile frame (and thus needs // re-parsing) if (toSave.getFramesetName().toLowerCase().equals( UserSettings.Username.toLowerCase())) { FrameUtils.ParseProfile(toSave); } } catch (IOException ioe) { ioe.printStackTrace(); Logger.Log(ioe); } return writer.getFileContents(); } public static Frame LoadProfile(String userName) { return LoadFrame(userName + FrameIO.getFramesetExtension(userName)); } public static Frame CreateNewProfile(String username) { Frame profile = CreateFrameset(username, PROFILE_PATH); FrameUtils.CreateDefaultProfile(profile); return profile; } /** * Reads the INF file that corresponds to the given Frame name * * @param framename * The Frame to lookup the INF file for * @throws IOException * Any exceptions encountered by the BufferedReader used to read * the INF. */ private static int ReadINF(String path, String frameset) throws IOException { assert (!frameset.endsWith(".")); // read INF BufferedReader reader; try { reader = new BufferedReader(new FileReader(path + frameset.toLowerCase() + File.separator + INF_FILENAME)); } catch (Exception e) { reader = new BufferedReader(new FileReader(path + frameset.toLowerCase() + File.separator + frameset.toLowerCase() + ".inf")); } String inf = reader.readLine(); reader.close(); return Conversion.getFrameNumber(inf); } /** * Writes the given String out to the INF file corresponding to the current * frameset. * * @param toWrite * The String to write to the file. * @throws IOException * Any exception encountered by the BufferedWriter. */ private static void WriteINF(String path, String frameset, String frameName) throws IOException { assert (!frameset.endsWith(".")); path += frameset.toLowerCase() + File.separator + INF_FILENAME; BufferedWriter writer = new BufferedWriter(new FileWriter(path)); writer.write(frameName); writer.close(); } public static boolean FrameIsCached(String name) { return _Cache.containsKey(name); } private static String NameValidation(String toValidate) { String result = ""; boolean capital = true; for (int i = 0; i < toValidate.length(); i++) { char cur = toValidate.charAt(i); // check for illegal characters if (ILLEGAL_CHARS.contains("" + cur)) { FrameGraphics .DisplayMessage("Frameset name contains illegal character '" + cur + "' at position " + (i + 1)); return null; } // capitalize all characters after spaces if (cur == ' ') { capital = true; } else { if (capital) { capital = false; result += ((String) "" + cur).toUpperCase(); } else result += cur; if (result.length() >= MAX_NAME_LENGTH) { FrameGraphics .DisplayMessage("Frameset name is too long (Max " + MAX_NAME_LENGTH + " characters)"); return null; } } } return result; } public static Frame CreateNewFrame(Item linker) { String title = null; if (linker instanceof Text) title = ((Text) linker).getFirstLine(); String frameset = DisplayIO.getCurrentFrame().getFramesetName(); String templateLink = linker.getLinkTemplate(); Frame newFrame = FrameIO.CreateFrame(frameset, title, templateLink); // do auto shrinking of the title Text titleItem = newFrame.getTitle(); while(titleItem.getBoundsWidth() + titleItem.getX() > newFrame.getFrameNameItem().getX() ) { titleItem.setSize(titleItem.getSize() - 1); } return newFrame; } /** * Creates a new Frameset on disk, including a .0, .1, and .inf files. The * Default.0 frame is copied to make the initial .0 and .1 Frames * * @param name * The Frameset name to use * @return The name of the first Frame in the newly created Frameset (the .1 * frame) */ public static Frame CreateNewFrameset(String name) { String path = DisplayIO.getCurrentFrame().path; // if current frameset is profile directory change it to framesets if (path.equals(FrameIO.PROFILE_PATH)) { path = FrameIO.FRAME_PATH; } return FrameIO.CreateFrameset(name, path); } /** * * @param frameset * @return */ public static int getLastNumber(String frameset) { // Rob thinks it might // have been // GetHighestNumExFrame // TODO minimise the number of frames being read in!! int num = -1; Frame zero = LoadFrame(frameset + "0"); // the frameset does not exist (or has no 0 frame) if (zero == null) return -1; try { num = ReadINF(zero.path, frameset); } catch (IOException e) { // TODO Auto-generated catch block // e.printStackTrace(); } /* * Michael doesnt think the code below is really needed... it will just * slow things down when we are reading frames over a network***** for (; * num >= 0; num--) { System.out.println("This code is loading frames to * find the highest existing frame..."); if (LoadFrame(frameset + num) != * null) break; } */ return num; } }