package com.customfit.ctg.data; import com.customfit.ctg.controller.Application; import com.customfit.ctg.model.*; import com.thoughtworks.xstream.XStream; import java.util.*; import java.io.*; /** * The FlatFileDriver class represents one of the DataDriverInterfaces * aimed at providing basic data operations for the application * for the local filesystem, as a data store. * * It uses the long-term object persistence provided in the JSR 57, * Long-Term Persistence for JavaBeans. Although it is the only * part of JavaBeans we'll be using here. * * @author David */ public class FlatFileDriver implements DataDriver { /** * This is appended to file names when they are generated * by the insert/update queries, when exact file names are * not provided specified by the driver consumer. It provides * a default naming convention for recipe files. */ public static final String RECIPE_FILE_SUFFIX = ".recipe.xml"; /** * This is appended to file names when they are generated * by the insert/update queries, when exact file names are * not provided specified by the driver consumer. It provides * a default naming convention for user files. */ public static final String USER_FILE_SUFFIX = ".user.xml"; /** * This is File path where recipe data will go, by default, per * the driver implementation. */ private File recipeDataDirectory; /** * This is File path where user data will go, by default, per * the driver implementation. */ private File userDataDirectory; /** * This uses the external XStream library to do what neither * Object serialization nor JAXB serialization can, that is * serialize everything including generic collections, etc. */ private XStream xStream = new XStream(); /** * Creates a new FlatFileDriver object and automatically connects * to the application's current working directory. */ public FlatFileDriver() { super(); //connect driver to application current working directory this.connect(); //map short names to objects for serialization xStream.alias("recipe", Recipe.class); xStream.alias("ingredient", RecipeIngredient.class); xStream.alias("user", User.class); xStream.alias("meal", Meal.class); xStream.alias("member", Member.class); } /** * Connects the driver to the specific data directory. * * Using this method call will ensure that all data will be written * to your specified directory. * * The destination directory is optional. By using the other * method overload, you guarantee the data directory will be * chosen by the driver. * * Once a driver has connected to directory, you must call connect(String) * to use a different directory for default file storage, or connect() * without any parameters for the application's current working directory. * * @param dataDirectory The directory you want to save to/read from * by default. * * @return Boolean indicating that you have read and write * permissions at the target directory you specified. */ @Override public boolean connect(String dataDirectory) { //this is a file system driver //but we'll do a little testing of the waters anyways this.recipeDataDirectory = new File(dataDirectory); this.userDataDirectory = new File(dataDirectory); //the required testing facility is in connect() return this.connect(); } /** * Connects the driver to the application's current working directory. * * The destination directory is optional by using the other function * overload. Once the object is created, the current working can be * refreshed by calling connect() again without any parameters. * * The default directory for recipes is within the scope of the\ * application's current working directory, at the path: * ./app_data/... * * Recipes are stored at: * ./app_data/recipes/* * * Once a driver is connected, the object will not adjust paths even * if the application's current working directory is changed through * another means. You must re-instantiate the driver or choose a data * directory manually by calling the connect(String) overloaded method. * * @return Boolean indicating that you have read and write permissions * at the directory you specified. */ public boolean connect() { //this is a file system driver //but we'll do a little testing of the waters anyways this.recipeDataDirectory = new File ("." + File.separator + "app_data" + File.separator + "recipes"); //recipes data directory this.userDataDirectory = new File ("." + File.separator + "app_data" + File.separator + "users"); //users data directory //the required testing facility is in isConnected() return this.isConnected(); } /** * Checks the driver's connection state to the current data directory. * * @return Boolean indicating that you have read and write permissions * at the directory it is connected to. */ @Override public boolean isConnected() { //this is a file system driver //but we'll do a little testing of the waters anyways boolean canConnect = false; //if the data directory exists if (recipeDataDirectory.exists()) { //and if you can read & write to data directory if (recipeDataDirectory.canWrite() && recipeDataDirectory.canRead()) //then you are "connected" canConnect = true; } //otherwise else { File cwd = new File ("."); //get current working directory //if you can read & write to current working directory if (cwd.exists() && cwd.canWrite() && cwd.canRead()) //then you are "connected" canConnect = true; } return canConnect; } /** * Doesn't do anything. It would close a connection normally, but * there isn't really a connection in this implementation. */ @Override public void close() { // Do nothing, although it is needed for other drivers to work // properly. } /** * Returns the current recipe data directory. * * It makes no assertions about the validity of this path. Use * the resulting object to check for existing paths and/or usability. * * @return The recipe data directory. */ public File getRecipeDataDirectory() { return this.recipeDataDirectory; } /** * Returns the current user data directory. * * It makes no assertions about the validity of this path. Use * the resulting object to check for existing paths and/or usability. * * @return The user data directory. */ public File getUserDataDirectory() { return this.userDataDirectory; } /** * Selects all recipes from the recipe data directory. * * @return A List of Recipe objects. */ @Override public List<Recipe> selectAllRecipes() { List<Recipe> recipes = new ArrayList<Recipe>(); if (recipeDataDirectory.exists()) for (File recipeFile : recipeDataDirectory.listFiles()) { Recipe recipe = null; //recipe storage recipe = this.selectRecipeByFile(recipeFile); if (recipe != null) recipes.add(recipe); } return recipes; } /** * Selects all recipes from the recipe data directory when * given a recipe name. * * @param recipeName Name of the recipe. File must be located in the * recipe data directory. The file must be suffixed with RECIPE_FILE_SUFFIX. * The recipe file associated with the name must exist, if it does not, the * method will return an empty list. * * @return List of Recipe objects as required by the interface, but * the list will only have one item in it, per the implementation. */ @Override public List<Recipe> selectRecipesByName(String recipeName) { //in this driver implementation, there will only be one in item in the list //but in SQL queries, there may be more entries //create list List<Recipe> recipes = new ArrayList<Recipe>(); //build recipe from file Recipe recipe = null; //recipe storage String recipeFileName = ""; try { recipeFileName = recipeDataDirectory.getCanonicalPath() + File.separator + recipeName + RECIPE_FILE_SUFFIX; } catch (IOException ex) { Application.dumpException("There was a problem opening target data directory for recipes.", ex); } File recipeFile = new File(recipeFileName); if (recipeFile.exists()) recipe = this.selectRecipeByFile(recipeFile); //add recipe to list recipes.add(recipe); //return return recipes; } /** * Returns a deserialized Recipe object from a XML File. * * @param recipeFile File containing Recipe object. * * @return Recipe object from state-file. */ public Recipe selectRecipeByFile(File recipeFile) { FileInputStream iStream = null; Recipe recipe = null; //recipe storage try { iStream = new FileInputStream(recipeFile); } catch (FileNotFoundException ex) { try { Application.dumpException("There was a problem opening non-existant file at " + recipeFile.getCanonicalPath() + " for deserialization. Not sure where it could have went, but it's gone now.", ex); } catch (IOException e) { Application.dumpException("There was a problem opening a non-existant file for deserialization. Not sure where it could have went, but it's gone now.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } } //NOTE to self: Ask Team Code Breakers if there would be any objection //to forking the code later to a side-project for a campus-wide Java EE //on the server/cloud--(if we even have a cloud, hopefully Ubuntu :~) and //Java FX in the web browser for the GUI. we could set up a launcher from //within radford.edu and/or the MyRU portal application, a future mobile app //for within the 3G/4G campus community, and several other useful features. //I could probably do it alone, but I admire community contributions. But //if you aren't in our trusted zone, you'd have to use an experimental //branch. Every one has a RU LDAP user name for campus-wide use. LDAP client //integration for authentication and identification is super simple to implement //so long as the server trusted it's application--I see no reason why it shouldn't //if it ran it from home or from within the cloud of known trusted servers. //I'd/We'd have to rewind all the way back to Analysis to implement this many //changes, however the processes can be made to be more closely integrated. //There must be a better way to view all of the combined-project phases than //this manual method we are using. However irrelevant to the fact that the //Rewind doesn't require a clean slate, but a complete overhaul to compensate //the technology differences. There are many students that can benefit from //our work(s). Since we've open-sourced the application, it can be componentized //to include the differences of other universities, junior colleges, high schools, //corporate communities, etc. Perhaps the easiest way to do that would be to program //for multiple languages, from the beginning-out. English (College) may have it's own //user interface grammar, so might English ( //use XStream now instead of JAXB recipe = (Recipe) this.xStream.fromXML(iStream); try { //close input stream if (iStream != null) iStream.close(); } catch (IOException ex) { try { Application.dumpException("There was a problem closing file at " + recipeFile.getCanonicalPath() + " after deserialization. Proceeding without interruption.", ex); } catch (IOException e) { Application.dumpException("There was a problem closing a file after deserialization. Proceeding without interruption.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } } return recipe; } /** * Inserts a new recipe XML file into the recipe data directory by * serializing the Recipe object. * * @param recipe Recipe to save. */ @Override public boolean insertRecipe(Recipe recipe) { //when using this routine, we'll auto-generate data directory if (!recipeDataDirectory.exists()) recipeDataDirectory.mkdirs(); //prepare a file File newFile; try { String newFileName = recipeDataDirectory.getCanonicalPath() + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX; newFile = new File(newFileName); } catch (IOException ex) { try { Application.dumpException("There was a problem creating file at " + recipeDataDirectory.getCanonicalPath() + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem creating file at " + "." + File.separator + "app_data" + File.separator + "recipes" + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } //now call the overload to export return this.insertRecipeToFile(recipe, newFile); } /** * Serializes Recipe to XML file you specify. * * @param recipe Recipe object to store. * @param toFile File to put it in. * * @return Boolean indicating success of the operation. */ public boolean insertRecipeToFile(Recipe recipe, File toFile) { //this is very reusable, especially for recipe file exporting FileOutputStream fOut; try { fOut = new FileOutputStream(toFile, false); } catch (FileNotFoundException ex) { try { Application.dumpException("There was a problem creating file at " + recipeDataDirectory.getCanonicalPath() + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem creating file at " + "." + File.separator + "app_data" + File.separator + "recipes" + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } //use XStream now instead of JAXB this.xStream.toXML(recipe, fOut); try { //close file fOut.close(); } catch (IOException ex) { try { Application.dumpException("There was a problem closing file at " + recipeDataDirectory.getCanonicalPath() + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem closing file at " + "." + File.separator + "app_data" + File.separator + "recipes" + File.separator + recipe.getName() + RECIPE_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } return true; } /** * Updates the recipe with the currentRecipeName with the new Recipe object. * * This may mean the file can be renamed, if the recipe name changes. * * @param currentRecipeName The current name of the recipe. (Before changing) * * @param updatedRecipe The Recipe object to save. (May have a new recipe name.) * * @return Boolean indicating the success of the operation. */ @Override public boolean updateRecipeByName(String currentRecipeName, Recipe updatedRecipe) { //for the filesystem driver, this couldn't be simpler //we're going to delete, then insert if (this.isConnected()) { if (this.deleteRecipeByName(currentRecipeName)) { //delete succeeded //now save it return this.insertRecipe(updatedRecipe); } } //otherwise: return false; } /** * Deletes the recipe using the object's default naming convention. * * Read about inserting for more information about naming conventions. * * @param recipeName Recipe name. * * @return Boolean indicating the success of the operation. */ @Override public boolean deleteRecipeByName(String recipeName) { File file; try { file = new File(recipeDataDirectory.getCanonicalPath() + File.separator + recipeName + RECIPE_FILE_SUFFIX); boolean status = this.deleteRecipeFile(file); try { Thread.currentThread().sleep(200); } catch (InterruptedException ex) { Application.dumpException("Thread sleep interrupted. Not a major problem either.", ex); } return status; } catch (IOException ex) { try { Application.dumpException("There was an error deleting the file " + recipeDataDirectory.getCanonicalPath() + File.separator + recipeName + RECIPE_FILE_SUFFIX, ex); } catch (IOException e) { Application.dumpException("There was an error deleting a file for the " + recipeName + "recipe.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } } /** * Deletes whatever file you specify. * * @param file Any valid File object. * * @return Boolean indicating the success of the operation. */ public boolean deleteRecipeFile(File file) { return file.delete(); } /** * Selects all recipes from the user data directory. * * @return A List of User objects. */ @Override public List<User> selectAllUsers() { List<User> users = new ArrayList<User>(); if (userDataDirectory.exists()) for (File userFile : userDataDirectory.listFiles()) { User user = null; //user storage user = this.selectUserByFile(userFile); if (user != null) users.add(user); } return users; } /** * Selects all users from the user data directory when * given a user name. * * @param userName Name of the user. File must be located in the * user data directory. The file must be suffixed with USER_FILE_SUFFIX. * The user file associated with the name must exist, if it does not, the * method will return an empty list. * * @return List of User objects as required by the interface, but * the list will only have one item in it, per the implementation. */ @Override public List<User> selectUsersByName(String userName) { //in this driver implementation, there will only be one in item in the list //but in SQL queries, there may be more entries //create list List<User> users = new ArrayList<User>(); //build user from file User user = null; //user storage String userFileName = ""; try { userFileName = userDataDirectory.getCanonicalPath() + File.separator + userName + USER_FILE_SUFFIX; } catch (IOException ex) { Application.dumpException("There was a problem opening target data directory for users.", ex); } File userFile = new File(userFileName); if (userFile.exists()) user = this.selectUserByFile(userFile); //add user to list users.add(user); //return return users; } /** * Returns a deserialized User object from a XML File. * * @param userFile File containing User object. * * @return User object from state-file. */ public User selectUserByFile(File userFile) { FileInputStream iStream = null; User user = null; //recipe storage try { iStream = new FileInputStream(userFile); } catch (FileNotFoundException ex) { try { Application.dumpException("There was a problem opening non-existant file at " + userFile.getCanonicalPath() + " for deserialization. Not sure where it could have went, but it's gone now.", ex); } catch (IOException e) { Application.dumpException("There was a problem opening a non-existant file for deserialization. Not sure where it could have went, but it's gone now.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } } //use XStream now instead of JAXB user = (User) this.xStream.fromXML(iStream); try { //close input stream if (iStream != null) iStream.close(); } catch (IOException ex) { try { Application.dumpException("There was a problem closing file at " + userFile.getCanonicalPath() + " after deserialization. Proceeding without interruption.", ex); } catch (IOException e) { Application.dumpException("There was a problem closing a file after deserialization. Proceeding without interruption.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } } return user; } /** * Inserts a new user XML file into the user data directory by * serializing the User object. * * @param user User to save. */ @Override public boolean insertUser(User user) { //when using this routine, we'll auto-generate data directory if (!userDataDirectory.exists()) userDataDirectory.mkdirs(); //prepare a file File newFile; try { String newFileName = userDataDirectory.getCanonicalPath() + File.separator + user.getName() + USER_FILE_SUFFIX; newFile = new File(newFileName); } catch (IOException ex) { try { Application.dumpException("There was a problem creating file at " + userDataDirectory.getCanonicalPath() + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem creating file at " + "." + File.separator + "app_data" + File.separator + "users" + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } //now call the overload to export return this.insertUserToFile(user, newFile); } /** * Serializes User to XML file you specify. * * @param user User object to store. * @param toFile File to put it in. * * @return Boolean indicating success of the operation. */ public boolean insertUserToFile(User user, File toFile) { //this is very reusable, especially for recipe file exporting FileOutputStream fOut; try { fOut = new FileOutputStream(toFile, false); } catch (FileNotFoundException ex) { try { Application.dumpException("There was a problem creating file at " + userDataDirectory.getCanonicalPath() + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem creating file at " + "." + File.separator + "app_data" + File.separator + "users" + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } //use XStream now instead of JAXB this.xStream.toXML(user, fOut); try { //close file fOut.close(); } catch (IOException ex) { try { Application.dumpException("There was a problem closing file at " + userDataDirectory.getCanonicalPath() + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); } catch (IOException e) { Application.dumpException("There was a problem closing file at " + "." + File.separator + "app_data" + File.separator + "users" + File.separator + user.getName() + USER_FILE_SUFFIX + ".", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } return true; } /** * Updates the user with the currentUserName with the new User object. * * This may mean the file can be renamed, if the user name changes. * * @param currentUserName The current name of the user. (Before changing) * @param updatedUser The User object to save. (May have a new user name.) * * @return Boolean indicating the success of the operation. */ @Override public boolean updateUserByName(String currentUserName, User updatedUser) { //for the filesystem driver, this couldn't be simpler //we're going to delete, then insert if (this.isConnected()) { if (this.deleteUserByName(currentUserName)) { //delete succeeded //now save it return this.insertUser(updatedUser); } } //otherwise: return false; } /** * Deletes the user using the object's default naming convention. * * Read about inserting for more information about naming conventions. * * @param userName User name. * * @return Boolean indicating the success of the operation. */ @Override public boolean deleteUserByName(String userName) { File file; try { file = new File(userDataDirectory.getCanonicalPath() + File.separator + userName + USER_FILE_SUFFIX); return this.deleteUserFile(file); } catch (IOException ex) { try { Application.dumpException("There was an error deleting the file " + userDataDirectory.getCanonicalPath() + File.separator + userName + USER_FILE_SUFFIX, ex); } catch (IOException e) { Application.dumpException("There was an error deleting a file for the " + userName + "recipe.", ex); Application.dumpException("Then an error was generated while generating the error.", e); } return false; } } /** * Deletes whatever file you specify. * * @param file Any valid File object. * * @return Boolean indicating the success of the operation. */ public boolean deleteUserFile(File file) { return file.delete(); } }