/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.history; import java.io.*; import java.util.*; import javax.xml.parsers.*; import net.java.sip.communicator.service.history.*; import net.java.sip.communicator.service.history.records.*; import net.java.sip.communicator.util.*; import org.jitsi.service.configuration.*; import org.jitsi.service.fileaccess.*; import org.osgi.framework.*; import org.w3c.dom.*; import org.xml.sax.*; /** * @author Alexander Pelov * @author Damian Minkov * @author Lubomir Marinov */ public class HistoryServiceImpl implements HistoryService { /** * The data directory. */ public static final String DATA_DIRECTORY = "history_ver1.0"; /** * The data file. */ public static final String DATA_FILE = "dbstruct.dat"; /** * The logger for this class. */ private static final Logger logger = Logger.getLogger(HistoryServiceImpl.class); // Note: Hashtable is SYNCHRONIZED private final Map<HistoryID, History> histories = new Hashtable<HistoryID, History>(); private final FileAccessService fileAccessService; private final DocumentBuilder builder; private final boolean cacheEnabled; /** * Characters and their replacement in created folder names */ private final static String[][] ESCAPE_SEQUENCES = new String[][] { {"&", "&_amp"}, {"/", "&_sl"}, {"\\\\", "&_bs"}, // the char \ {":", "&_co"}, {"\\*", "&_as"}, // the char * {"\\?", "&_qm"}, // the char ? {"\"", "&_pa"}, // the char " {"<", "&_lt"}, {">", "&_gt"}, {"\\|", "&_pp"} // the char | }; /** * Constructor. * * @param bundleContext OSGi bundle context * @throws Exception if something went wrong during initialization */ public HistoryServiceImpl(BundleContext bundleContext) throws Exception { this.builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); this.cacheEnabled = getConfigurationService(bundleContext).getBoolean( CACHE_ENABLED_PROPERTY, false); this.fileAccessService = getFileAccessService(bundleContext); } public Iterator<HistoryID> getExistingIDs() { List<File> vect = new Vector<File>(); File histDir; try { String userSetDataDirectory = System.getProperty("HistoryServiceDirectory"); histDir = getFileAccessService().getPrivatePersistentDirectory( (userSetDataDirectory == null) ? DATA_DIRECTORY : userSetDataDirectory, FileCategory.PROFILE); findDatFiles(vect, histDir); } catch (Exception e) { logger.error("Error opening directory", e); } DBStructSerializer structParse = new DBStructSerializer(this); for (File f : vect) { synchronized (this.histories) { try { History hist = structParse.loadHistory(f); if (!this.histories.containsKey(hist.getID())) { this.histories.put(hist.getID(), hist); } } catch (Exception e) { logger.error("Could not load history from file: " + f.getAbsolutePath(), e); } } } synchronized (this.histories) { return this.histories.keySet().iterator(); } } public boolean isHistoryExisting(HistoryID id) { return this.histories.containsKey(id); } public History getHistory(HistoryID id) throws IllegalArgumentException { History retVal = null; synchronized (this.histories) { if (histories.containsKey(id)) { retVal = histories.get(id); } else { throw new IllegalArgumentException( "No history corresponds to the specified ID."); } } return retVal; } public History createHistory( HistoryID id, HistoryRecordStructure recordStructure) throws IllegalArgumentException, IOException { History retVal = null; synchronized (this.histories) { if (this.histories.containsKey(id)) { retVal = this.histories.get(id); retVal.setHistoryRecordsStructure(recordStructure); } else { File dir = this.createHistoryDirectories(id); HistoryImpl history = new HistoryImpl(id, dir, recordStructure, this); File dbDatFile = new File(dir, HistoryServiceImpl.DATA_FILE); DBStructSerializer dbss = new DBStructSerializer(this); dbss.writeHistory(dbDatFile, history); this.histories.put(id, history); retVal = history; } } return retVal; } protected FileAccessService getFileAccessService() { return this.fileAccessService; } protected DocumentBuilder getDocumentBuilder() { return builder; } /** * Parse documents. Synchronized to avoid exception * when concurrently parsing with same DocumentBuilder * @param file File the file to parse * @return Document the result document * @throws SAXException exception * @throws IOException exception */ protected synchronized Document parse(File file) throws SAXException, IOException { FileInputStream fis = new FileInputStream(file); Document doc = builder.parse(fis); fis.close(); return doc; } /** * Parse documents. Synchronized to avoid exception * when concurrently parsing with same DocumentBuilder * @param in ByteArrayInputStream the stream to parse * @return Document the result document * @throws SAXException exception * @throws IOException exception */ protected synchronized Document parse(ByteArrayInputStream in) throws SAXException, IOException { return builder.parse(in); } private void findDatFiles(List<File> vect, File directory) { File[] files = directory.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { findDatFiles(vect, files[i]); } else if (DATA_FILE.equalsIgnoreCase(files[i].getName())) { vect.add(files[i]); } } } private File createHistoryDirectories(HistoryID id) throws IOException { String[] idComponents = id.getID(); // escape chars in directory names escapeCharacters(idComponents); String userSetDataDirectory = System.getProperty("HistoryServiceDirectory"); File dir = new File(userSetDataDirectory != null ? userSetDataDirectory : DATA_DIRECTORY); for (String s : idComponents) { dir = new File(dir, s); } File directory = null; try { directory = getFileAccessService().getPrivatePersistentDirectory( dir.toString(), FileCategory.PROFILE); } catch (Exception e) { IOException ioe = new IOException( "Could not create history due to file system error"); ioe.initCause(e); throw ioe; } if (!directory.exists() && !directory.mkdirs()) { throw new IOException( "Could not create requested history service files:" + directory.getAbsolutePath()); } return directory; } /** * Returns whether caching of readed documents is enabled or desibled. * @return boolean */ protected boolean isCacheEnabled() { return cacheEnabled; } /** * Permamently removes local stored History * * @param id HistoryID * @throws IOException */ public void purgeLocallyStoredHistory(HistoryID id) throws IOException { // get the history directory corresponding the given id File dir = this.createHistoryDirectories(id); if (logger.isTraceEnabled()) logger.trace("Removing history directory " + dir); deleteDirAndContent(dir); History history = histories.remove(id); if(history == null) { // well this can be global delete, so lets remove all matching // sub-histories String[] ids = id.getID(); Iterator<Map.Entry<HistoryID, History>> iter = histories.entrySet().iterator(); while(iter.hasNext()) { Map.Entry<HistoryID, History> entry = iter.next(); if(isSubHistory(ids, entry.getKey())) { iter.remove(); } } } } /** * Clears locally(in memory) cached histories. */ public void purgeLocallyCachedHistories() { histories.clear(); } /** * Checks the ids of the parent, do they exist in the supplied history ids. * If it exist the history is sub history of the on with the supplied ids. * @param parentIDs the parent ids * @param hid the history to check * @return whether history is sub one (contained) of the parent. */ private boolean isSubHistory(String[] parentIDs, HistoryID hid) { String[] hids = hid.getID(); if(hids.length < parentIDs.length) return false; for(int i = 0; i < parentIDs.length; i++) { if(!parentIDs[i].equals(hids[i])) return false; } // everything matches, return true return true; } /** * Deletes given directory and its content * * @param dir File * @throws IOException */ private void deleteDirAndContent(File dir) throws IOException { if(!dir.isDirectory()) return; File[] content = dir.listFiles(); File tmp; for (int i = 0; i < content.length; i++) { tmp = content[i]; if(tmp.isDirectory()) deleteDirAndContent(tmp); else tmp.delete(); } dir.delete(); } /** * Replacing the characters that we must escape * used for the created filename. * * @param ids Ids - folder names as we are using * FileSystem for storing files. */ private void escapeCharacters(String[] ids) { for (int i = 0; i < ids.length; i++) { String currId = ids[i]; for (int j = 0; j < ESCAPE_SEQUENCES.length; j++) { currId = currId. replaceAll(ESCAPE_SEQUENCES[j][0], ESCAPE_SEQUENCES[j][1]); } ids[i] = currId; } } private static ConfigurationService getConfigurationService( BundleContext bundleContext) { ServiceReference serviceReference = bundleContext.getServiceReference(ConfigurationService.class .getName()); return (serviceReference == null) ? null : (ConfigurationService) bundleContext.getService(serviceReference); } private static FileAccessService getFileAccessService( BundleContext bundleContext) { return ServiceUtils.getService(bundleContext, FileAccessService.class); } /** * Moves the content of oldId history to the content of the newId. * Moves the content from the oldId folder to the newId folder. * Old folder must exist. * * @param oldId old and existing history * @param newId the place where content of oldId will be moved * @throws java.io.IOException problem moving to newId */ public void moveHistory(HistoryID oldId, HistoryID newId) throws IOException { if(!isHistoryCreated(oldId))// || !isHistoryExisting(newId)) return; File oldDir = this.createHistoryDirectories(oldId); File newDir = getDirForHistory(newId); // make sure parent path is existing newDir.getParentFile().mkdirs(); if(!oldDir.renameTo(newDir)) { if (logger.isInfoEnabled()) logger.info("Cannot move history!"); throw new IOException("Cannot move history!"); } histories.remove(oldId); } /** * Returns the folder for the given history without creating it. * @param id the history * @return the folder for the history */ private File getDirForHistory(HistoryID id) { // put together subfolder names. String[] dirNames = id.getID(); StringBuffer dirName = new StringBuffer(); for (int i = 0; i < dirNames.length; i++) { if (i > 0) dirName.append(File.separatorChar); dirName.append(dirNames[i]); } // get the parent directory File histDir = null; try { String userSetDataDirectory = System.getProperty("HistoryServiceDirectory"); histDir = getFileAccessService().getPrivatePersistentDirectory( (userSetDataDirectory == null) ? DATA_DIRECTORY : userSetDataDirectory, FileCategory.PROFILE); } catch (Exception e) { logger.error("Error opening directory", e); } return new File(histDir, dirName.toString()); } /** * Checks whether a history is created and stored. * Exists in the file system. * @param id the history to check * @return whether a history is created and stored. */ public boolean isHistoryCreated(HistoryID id) { return getDirForHistory(id).exists(); } /** * Enumerates existing histories. * @param rawid the start of the HistoryID of all the histories that will be * returned. * @return list of histories which HistoryID starts with <tt>rawid</tt>. * @throws IllegalArgumentException if the <tt>rawid</tt> contains ids * which are missing in current history. */ public List<HistoryID> getExistingHistories( String[] rawid) throws IllegalArgumentException { File histDir = null; try { histDir = getFileAccessService() .getPrivatePersistentDirectory( DATA_DIRECTORY, FileCategory.PROFILE); } catch (Exception e) { logger.error("Error opening directory", e); } if(histDir == null || !histDir.exists()) return new ArrayList<HistoryID>(); StringBuilder folderPath = new StringBuilder(); for(String id : rawid) folderPath.append(id).append(File.separator); File srcFolder = new File(histDir, folderPath.toString()); if(!srcFolder.exists()) return new ArrayList<HistoryID>(); TreeMap<File, HistoryID> recentFiles = new TreeMap<File, HistoryID>(new Comparator<File>() { @Override public int compare(File o1, File o2) { return o1.getName().compareTo(o2.getName()); } }); getExistingFiles(srcFolder, Arrays.asList(rawid), recentFiles); // return non duplicate List<HistoryID> result = new ArrayList<HistoryID>(); for(Map.Entry<File, HistoryID> entry : recentFiles.entrySet()) { HistoryID hid = entry.getValue(); if(result.contains(hid)) continue; result.add(hid); } return result; } /** * Get existing files in <tt>res</tt> and their corresponding historyIDs. * @param sourceFolder the folder to search into. * @param rawID the rawID. * @param res the result map. */ private void getExistingFiles( File sourceFolder, List<String> rawID, Map<File, HistoryID> res) { for(File f : sourceFolder.listFiles()) { if(f.isDirectory()) { List<String> newRawID = new ArrayList<String>(rawID); newRawID.add(f.getName()); getExistingFiles(f, newRawID, res); } else { if(f.getName().equals(DATA_FILE)) continue; res.put(f, HistoryID.createFromRawStrings( rawID.toArray(new String[rawID.size()]))); } } } }