package com.constellio.data.dao.managers.config; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.TreeMap; import org.apache.commons.io.FileUtils; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.constellio.data.dao.managers.StatefulService; import com.constellio.data.dao.managers.config.ConfigManagerException.OptimisticLockingConfiguration; import com.constellio.data.dao.managers.config.ConfigManagerRuntimeException.ConfigurationAlreadyExists; import com.constellio.data.dao.managers.config.ConfigManagerRuntimeException.NoSuchConfiguration; import com.constellio.data.dao.managers.config.events.ConfigEventListener; import com.constellio.data.dao.managers.config.events.ConfigUpdatedEventListener; import com.constellio.data.dao.managers.config.values.BinaryConfiguration; import com.constellio.data.dao.managers.config.values.PropertiesConfiguration; import com.constellio.data.dao.managers.config.values.TextConfiguration; import com.constellio.data.dao.managers.config.values.XMLConfiguration; import com.constellio.data.io.services.facades.IOServices; import com.constellio.data.io.streamFactories.StreamFactory; import com.constellio.data.utils.ImpossibleRuntimeException; import com.constellio.data.utils.KeyListMap; import com.constellio.data.utils.PropertyFileUtils; import com.constellio.data.utils.hashing.HashingService; import com.constellio.data.utils.hashing.HashingServiceException; public class FileSystemConfigManager implements StatefulService, ConfigManager { static final String ADD_BINARY_FILE = "FileSystemConfigManager-AddBinaryFile"; static final String UPDATE_BINARY_FILE = "FileSystemConfigManager-UpdateBinaryFile"; static final String READ_BINARY_FILE = "FileSystemConfigManager-ReadBinaryFile"; static final String ADD_PROPERTIES_FILE = "FileSystemConfigManager-AddPropertiesFile"; static final String UPDATE_PROPERTIES_FILE = "FileSystemConfigManager-UpdatePropertiesFile"; static final String READ_PROPERTIES_FILE = "FileSystemConfigManager-ReadPropertiesFile"; final String READ_PROPERTIES_FILE_2 = "FileSystemConfigManager-ReadPropertiesFile2"; private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemConfigManager.class); private static final String NO_VERSION = "-1"; private final File configFolder; private final HashingService hashService; private final IOServices ioServices; private final Map<String, Object> cache = new HashMap<>(); private final KeyListMap<String, ConfigUpdatedEventListener> updatedConfigEventListeners = new KeyListMap<>(); public FileSystemConfigManager(File configFolder, IOServices ioServices, HashingService hashService) { super(); this.configFolder = configFolder; this.ioServices = ioServices; this.hashService = hashService; } @Override public void initialize() { } Map<String, Object> getCache() { return cache; } @Override public synchronized void add(String path, Document newDocument) { validateFileNonExistance(path); LOGGER.debug("add document => " + path); String content = getContentOfDocument(newDocument); try { ioServices.replaceFileContent(new File(configFolder, path), content); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("replace file content", e); } } @Override public synchronized void add(String path, InputStream newBinaryStream) { validateFileNonExistance(path); LOGGER.debug("add inputstream => " + path); File binFile = createFoldersAndFiles(path); try { ioServices.replaceFileContent(binFile, newBinaryStream); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("copy binary to file", e); } } File createFoldersAndFiles(String path) { LOGGER.debug("create folder => " + path); File file = new File(configFolder, path); file.getParentFile().mkdirs(); return file; } @Override public synchronized void add(String path, Map<String, String> newProperties) { validateFileNonExistance(path); LOGGER.debug("add properties => " + path); File propertiesFile = createFoldersAndFiles(path); File tempPropertiesFile = ioServices.getAtomicWriteTempFileFor(propertiesFile); Properties properties = mapToProperties(newProperties); OutputStream outputStream = null; try { outputStream = ioServices.newFileOutputStream(tempPropertiesFile, ADD_PROPERTIES_FILE); PropertyFileUtils.store(properties, outputStream); } catch (IOException e) { ioServices.deleteQuietly(tempPropertiesFile); throw new ConfigManagerRuntimeException.CannotCompleteOperation("store properties", e); } finally { ioServices.closeQuietly(outputStream); } ioServices.moveFile(tempPropertiesFile, propertiesFile); } Properties newProperties() { return new Properties(); } @Override public synchronized void delete(String path) { LOGGER.debug("delete document => " + path); cache.remove(path); new File(configFolder, path).delete(); } @Override public synchronized void deleteFolder(String path) { LOGGER.debug("delete folder => " + path); // TODO Remove from cache? Are folders in cache?? File folderToDelete = new File(configFolder, path); try { FileUtils.deleteDirectory(folderToDelete); } catch (IOException e) { // TODO Proper exception throw new RuntimeException(e); } } @Override public synchronized void delete(String path, String hash) throws OptimisticLockingConfiguration { LOGGER.debug("delete document => " + path); String content; try { content = readFile(path); } catch (ConfigManagerRuntimeException.NoSuchConfiguration e) { LOGGER.debug("Nothing to delete", e); return; } String expectedVersion = ""; try { expectedVersion = hashService.getHashFromString(content); } catch (HashingServiceException e) { throw new ConfigManagerRuntimeException.CannotHashTheFile(path, e); } validateHash(path, hash, expectedVersion); cache.remove(path); new File(configFolder, path).delete(); } @Override public synchronized boolean exist(String path) { File file = new File(configFolder, path); return file.exists() && file.isFile(); } @Override public synchronized boolean folderExist(String path) { File file = new File(configFolder, path); return file.exists() && file.isDirectory(); } @Override public List<String> list(String path) { List<String> fileNames = new ArrayList<>(); File file = new File(configFolder, path); if (file.exists()) { fileNames.addAll(Arrays.asList(file.list())); } return fileNames; } @Override public synchronized BinaryConfiguration getBinary(String path) { LOGGER.debug("get binary => " + path); if (!exist(path)) { return null; } StreamFactory<InputStream> binaryStreamFactory = ioServices .newInputStreamFactory(new File(configFolder, path), READ_BINARY_FILE); InputStream inputStream = null; try { inputStream = binaryStreamFactory.create(getClass().getName() + ".getBinary"); return new BinaryConfiguration(hashService.getHashFromBytes(ioServices.readBytes(inputStream)), binaryStreamFactory); } catch (HashingServiceException e) { throw new ConfigManagerRuntimeException.CannotHashTheFile(path, e); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("read binary file", e); } finally { ioServices.closeQuietly(inputStream); } } String getContentOfDocument(Document doc) { XMLOutputter xmlOutput = new XMLOutputter(Format.getPrettyFormat()); return xmlOutput.outputString(doc); } Document getDocumentFromFile(File file) { SAXBuilder builder = newSAXBuilder(); try { return builder.build(file); } catch (JDOMException e) { throw new ConfigManagerRuntimeException("JDOM2 Exception", e); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("build Document JDOM2 from file", e); } } @Override public synchronized PropertiesConfiguration getProperties(String path) { LOGGER.debug("get properties => " + path); if (!exist(path)) { return null; } File propertiesFile = new File(configFolder, path); Properties properties = newProperties(); InputStream inputStream = null; try { inputStream = ioServices.newFileInputStream(propertiesFile, READ_PROPERTIES_FILE); properties.load(inputStream); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("load the properties file", e); } finally { ioServices.closeQuietly(inputStream); } Map<String, String> mapProperties = propertiesToMap(properties); inputStream = null; try { inputStream = ioServices.newFileInputStream(propertiesFile, READ_PROPERTIES_FILE_2); return new PropertiesConfiguration(hashService.getHashFromBytes(ioServices.readBytes(inputStream)), mapProperties); } catch (HashingServiceException e) { throw new ConfigManagerRuntimeException.CannotHashTheFile(path, e); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("load the properties file", e); } finally { ioServices.closeQuietly(inputStream); } } @Override public synchronized XMLConfiguration getXML(String path) { LOGGER.debug("get XML => " + path); XMLConfiguration cachedConfiguration = (XMLConfiguration) cache.get(path); if (cachedConfiguration == null) { try { cachedConfiguration = readXML(path); } catch (NoSuchConfiguration noSuchConfiguration) { return null; } cache.put(path, cachedConfiguration); } return cachedConfiguration; } private XMLConfiguration readXML(String path) throws NoSuchConfiguration { String fileContent = readFile(path); String hash; try { hash = hashService.getHashFromString(fileContent); } catch (HashingServiceException e) { throw new ConfigManagerRuntimeException.CannotHashTheFile(path, e); } Document doc = getDocumentFromFile(new File(configFolder, path)); String version = readVersion(doc); if (version.equals(NO_VERSION)) { XMLConfiguration config = new XMLConfiguration(hash, hash, doc); cache.put(path, config); return config; } else { return new XMLConfiguration(version, hash, doc); } } String readVersion(Document doc) { Element root = doc.getRootElement(); String version = root.getAttributeValue("version"); if (version == null) { return NO_VERSION; } else { return version; } } SAXBuilder newSAXBuilder() { return new SAXBuilder(); } private String readFile(String path) throws NoSuchConfiguration { validateFileExistance(path); String content = ""; try { content = ioServices.readFileToString(new File(configFolder, path)); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("read file content", e); } return content; } Map<String, String> propertiesToMap(Properties properties) { Map<String, String> mapOfProperties = new HashMap<String, String>(); Enumeration<?> enumeration = properties.propertyNames(); while (enumeration.hasMoreElements()) { String key = (String) enumeration.nextElement(); String value = properties.getProperty(key); mapOfProperties.put(key, value); } return mapOfProperties; } @Override public synchronized void update(String path, String version, Document newDocument) throws OptimisticLockingConfiguration { LOGGER.debug("update document => " + path + " to version => " + version); File xmlFile = new File(configFolder, path); validateFileExistance(path); XMLConfiguration xmlConfigurationOfPath = this.getXML(path); String expectedVersion = xmlConfigurationOfPath.getHash(); validateHash(path, version, expectedVersion); if (expectedVersion.equals(readVersion(newDocument)) && newDocument != xmlConfigurationOfPath.getDocument()) { throw new ConfigManagerRuntimeException.WrongVersion(expectedVersion); } else { try { String content = getContentOfDocument(newDocument); ioServices.replaceFileContent(xmlFile, content); cache.remove(path); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("replace File Content", e); } } for (ConfigUpdatedEventListener listener : updatedConfigEventListeners.get(path)) { listener.onConfigUpdated(path); } } @Override public synchronized void update(String path, String version, InputStream newBinaryStream) throws OptimisticLockingConfiguration { LOGGER.debug("update inputstream => " + path + " to version => " + version); validateFileExistance(path); BinaryConfiguration binaryConfigurationOfPath = this.getBinary(path); String expectedVersion = binaryConfigurationOfPath.getHash(); validateHash(path, version, expectedVersion); try { ioServices.replaceFileContent(new File(configFolder, path), newBinaryStream); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("copy binary stream to file", e); } for (ConfigUpdatedEventListener listener : updatedConfigEventListeners.get(path)) { listener.onConfigUpdated(path); } } @Override public synchronized void update(String path, String version, Map<String, String> newProperties) throws OptimisticLockingConfiguration { LOGGER.debug("update properties => " + path + " to version => " + version); validateFileExistance(path); File propertiesFile = new File(configFolder, path); File tempPropertiesFile = ioServices.getAtomicWriteTempFileFor(propertiesFile); Properties properties = mapToProperties(newProperties); PropertiesConfiguration propertiesConfigurationOfPath = this.getProperties(path); String expectedVersion = propertiesConfigurationOfPath.getHash(); validateHash(path, version, expectedVersion); OutputStream outputStream = null; try { outputStream = ioServices.newFileOutputStream(tempPropertiesFile, UPDATE_PROPERTIES_FILE); PropertyFileUtils.store(properties, outputStream); } catch (IOException e) { ioServices.deleteQuietly(tempPropertiesFile); throw new ConfigManagerRuntimeException.CannotCompleteOperation("Write to properties file", e); } finally { ioServices.closeQuietly(outputStream); } ioServices.moveFile(tempPropertiesFile, propertiesFile); for (ConfigUpdatedEventListener listener : updatedConfigEventListeners.get(path)) { listener.onConfigUpdated(path); } } @Override public void registerListener(String path, ConfigEventListener listener) { if (listener instanceof ConfigUpdatedEventListener) { this.updatedConfigEventListeners.add(path, (ConfigUpdatedEventListener) listener); } } private Properties mapToProperties(Map<String, String> newProperties) { TreeMap<String, String> newPropertiesSorted = new TreeMap<String, String>(newProperties); Properties properties = newProperties(); properties.putAll(newPropertiesSorted); return properties; } @Override public synchronized void updateXML(String path, DocumentAlteration documentAlteration) { XMLConfiguration xmlConfiguration = this.getXML(path); Document doc = xmlConfiguration.getDocument(); documentAlteration.alter(doc); try { update(path, xmlConfiguration.getHash(), doc); } catch (OptimisticLockingConfiguration e) { throw new ImpossibleRuntimeException(e); } cache.remove(path); } @Override public synchronized void updateProperties(String path, PropertiesAlteration propertiesAlteration) { PropertiesConfiguration propertiesConfiguration = this.getProperties(path); Map<String, String> doc = propertiesConfiguration.getProperties(); propertiesAlteration.alter(doc); try { update(path, propertiesConfiguration.getHash(), doc); } catch (OptimisticLockingConfiguration e) { throw new ImpossibleRuntimeException(e); } cache.remove(path); } @Override public void createXMLDocumentIfInexistent(String path, DocumentAlteration documentAlteration) { if (!exist(path)) { Document document = new Document(); documentAlteration.alter(document); try { add(path, document); } catch (ConfigurationAlreadyExists e) { LOGGER.info("Configuration was created by another instance", e); } } } @Override public void createPropertiesDocumentIfInexistent(String path, PropertiesAlteration propertiesAlteration) { if (!exist(path)) { Map<String, String> properties = new HashMap<>(); propertiesAlteration.alter(properties); try { add(path, properties); } catch (ConfigurationAlreadyExists e) { LOGGER.info("Configuration was created by another instance", e); } } } @Override public void deleteAllConfigsIn(String collection) { List<String> toDelete = new ArrayList<>(); for (Entry<String, Object> entry : cache.entrySet()) { if (entry.getKey().startsWith(collection)) { toDelete.add(entry.getKey()); } } for (String collectionConfig : toDelete) { cache.remove(collectionConfig); } File folderCollection = new File(configFolder, collection); ioServices.deleteDirectoryWithoutExpectableIOException(folderCollection); } @Override public synchronized void copySettingsFrom(File setting) { try { this.ioServices.copyDirectory(setting, this.configFolder); cache.clear(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void move(String src, String dest) { if (!exist(src)) { throw new NoSuchConfiguration(src); } if (exist(dest)) { throw new ConfigurationAlreadyExists(dest); } File srcFile = new File(configFolder, src); File destFile = new File(configFolder, dest); if (!srcFile.renameTo(destFile)) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("move '" + src + "' to '" + dest + "'", null); } } private void validateFileExistance(String path) { if (!exist(path)) { throw new NoSuchConfiguration(path); } } private void validateFileNonExistance(String path) { if (exist(path)) { throw new ConfigManagerRuntimeException.ConfigurationAlreadyExists(path); } } void validateHash(String path, String version, String expectedVersion) throws OptimisticLockingConfiguration { if (!version.equals(expectedVersion)) { throw new ConfigManagerException.OptimisticLockingConfiguration(path, expectedVersion, version); } } @Override public void close() { } public File getConfigFolder() { return configFolder; } @Override public TextConfiguration getText(String path) { LOGGER.debug("get Text => " + path); TextConfiguration cachedConfiguration = null;// (XMLConfiguration) cache.get(path); if (cachedConfiguration == null) { try { String content = readFile(path); String version = hashService.getHashFromString(content); cachedConfiguration = new TextConfiguration(version, content); } catch (NoSuchConfiguration | HashingServiceException noSuchConfiguration) { return null; } cache.put(path, cachedConfiguration); } return cachedConfiguration; } }