package com.constellio.data.dao.managers.config; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import org.apache.commons.io.IOUtils; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.api.CuratorEvent; import org.apache.curator.framework.api.CuratorListener; import org.apache.curator.framework.recipes.locks.InterProcessMutex; import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock; import org.apache.curator.retry.ExponentialBackoffRetry; import org.apache.curator.utils.CloseableUtils; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.BadVersionException; import org.apache.zookeeper.KeeperException.NoNodeException; import org.apache.zookeeper.data.Stat; import org.jdom2.Document; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; 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.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.utils.KeyListMap; public class ZooKeeperConfigManager implements StatefulService, ConfigManager, CuratorListener { private static final String GET_BINARY_CONTENT = "ZooKeeperConfigManager-getBinaryContent"; private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperConfigManager.class); private final KeyListMap<String, ConfigUpdatedEventListener> updatedConfigEventListeners = new KeyListMap<>(); private CuratorFramework client; private String CONFIG_FOLDER = "/constellio"; private String address; private IOServices ioServices; public ZooKeeperConfigManager(String address, IOServices ioServices) { this.address = address; this.ioServices = ioServices; } @Override public void initialize() { try { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); client = CuratorFrameworkFactory.newClient(address, retryPolicy); client.start(); client.getCuratorListenable().addListener(this); } catch (Exception e) { CloseableUtils.closeQuietly(client); throw new RuntimeException("Zookeeper exception"); } } @Override public BinaryConfiguration getBinary(String path) { String tmpPath = processPath(CONFIG_FOLDER, path); byte[] ret; if (!exist(tmpPath)) { return null; } try { InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, tmpPath); InterProcessMutex readLock = lock.readLock(); readLock.acquire(); ret = client.getData().watched().forPath(tmpPath); readLock.release(); return new BinaryConfiguration(getVersion(tmpPath), ioServices.newByteArrayStreamFactory(ret, GET_BINARY_CONTENT)); } catch (NoNodeException e) { return null; } catch (Exception e) { throw new RuntimeException(e); } } @Override public XMLConfiguration getXML(String path) { String tmpPath = processPath(CONFIG_FOLDER, path); byte[] ret; if (!exist(tmpPath)) { return null; } try { InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, tmpPath); InterProcessMutex readLock = lock.readLock(); readLock.acquire(); ret = client.getData().watched().forPath(tmpPath); Document configuration = getDocumentFrom(ret); readLock.release(); return new XMLConfiguration(getVersion(tmpPath), null, configuration); } catch (NoNodeException e) { return null; } catch (Exception e) { throw new RuntimeException(e); } } @Override public PropertiesConfiguration getProperties(String path) { String tmpPath = processPath(CONFIG_FOLDER, path); byte[] ret; try { client.sync().forPath(CONFIG_FOLDER); } catch (Exception e) { throw new RuntimeException(e); } if (!exist(tmpPath)) { return null; } try { InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, tmpPath); InterProcessMutex readLock = lock.readLock(); readLock.acquire(); ret = client.getData().watched().forPath(tmpPath); Properties properties = new Properties(); ByteArrayInputStream input = new ByteArrayInputStream(ret); properties.load(input); input.close(); readLock.release(); return new PropertiesConfiguration(getVersion(tmpPath), propertiesToMap(properties)); } catch (NoNodeException e) { return null; } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean exist(String path) { String tmpPath = processPath(CONFIG_FOLDER, path); try { Stat stat = client.checkExists().forPath(tmpPath); return stat != null; } catch (KeeperException.NoNodeException e) { return false; } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean folderExist(String path) { throw new UnsupportedOperationException(); } @Override public List<String> list(String path) { throw new UnsupportedOperationException("TODO"); } @Override public void createXMLDocumentIfInexistent(String path, DocumentAlteration documentAlteration) { String tmpPath = processPath(CONFIG_FOLDER, path); if (!exist(tmpPath)) { Document newDocument = new Document(); documentAlteration.alter(newDocument); try { this.add(path, newDocument); } catch (ConfigManagerRuntimeException.ConfigurationAlreadyExists e) { LOGGER.info("Configuration was created by another instance", e); } } } @Override public void createPropertiesDocumentIfInexistent(String path, PropertiesAlteration propertiesAlteration) { String tmpPath = processPath(CONFIG_FOLDER, path); if (!exist(tmpPath)) { Map<String, String> mapProperties = new HashMap<>(); propertiesAlteration.alter(mapProperties); try { this.add(path, mapProperties); } catch (ConfigManagerRuntimeException.ConfigurationAlreadyExists e) { LOGGER.info("Configuration was created by another instance", e); } } } @Override // the update must be atomic (no call to external method who access ZooKeeper) to keep lock integrity public void updateXML(String path, DocumentAlteration documentAlteration) { String tmpPath = processPath(CONFIG_FOLDER, path); InterProcessMutex lock = lockPath(tmpPath); try { // get current version byte[] ret = client.getData().forPath(tmpPath); Document document = getDocumentFrom(ret); documentAlteration.alter(document); // update the data and get the new bytes int version = Integer.parseInt(getVersion(tmpPath)); byte[] bytes = getByteFromDocument(document); client.setData().withVersion(version).forPath(tmpPath, bytes); unlockPath(lock); } catch (OptimisticLockingConfiguration e) { unlockPath(lock); updateXML(path, documentAlteration); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override // the update must be atomic (no call to external method who access ZooKeeper) to keep lock integrity public void updateProperties(String path, PropertiesAlteration propertiesAlteration) { String tmpPath = processPath(CONFIG_FOLDER, path); InterProcessMutex lock = lockPath(tmpPath); try { // get current version byte[] ret = client.getData().forPath(tmpPath); Properties properties = new Properties(); ByteArrayInputStream input = new ByteArrayInputStream(ret); properties.load(input); input.close(); int version = Integer.parseInt(getVersion(tmpPath)); // update the data and get the new bytes Map<String, String> mapProperties = propertiesToMap(properties); propertiesAlteration.alter(mapProperties); Properties prop = mapToProperties(mapProperties); ByteArrayOutputStream output = new ByteArrayOutputStream(); prop.store(output, null); client.setData().withVersion(version).forPath(tmpPath, output.toByteArray()); unlockPath(lock); } catch (OptimisticLockingConfiguration e) { unlockPath(lock); updateProperties(path, propertiesAlteration); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void add(String path, InputStream newBinaryStream) { String tmpPath = processPath("/constellio", path); try { byte[] bytes = IOUtils.toByteArray(newBinaryStream); client.create().creatingParentsIfNeeded().forPath(tmpPath, bytes); } catch (IOException e) { throw new RuntimeException(e); } catch (KeeperException.NodeExistsException e) { throw new ConfigManagerRuntimeException.ConfigurationAlreadyExists(path); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void add(String path, Document newDocument) { String tmpPath = processPath("/constellio", path); try { byte[] bytes = getByteFromDocument(newDocument); client.create().creatingParentsIfNeeded().forPath(tmpPath, bytes); } catch (IOException e) { throw new RuntimeException(e); } catch (KeeperException.NodeExistsException e) { throw new ConfigManagerRuntimeException.ConfigurationAlreadyExists(path); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void add(String path, Map<String, String> newProperties) { String tmpPath = processPath("/constellio", path); try { Properties prop = mapToProperties(newProperties); ByteArrayOutputStream output = new ByteArrayOutputStream(); prop.store(output, null); client.create().creatingParentsIfNeeded().forPath(tmpPath, output.toByteArray()); } catch (IOException e) { throw new RuntimeException(e); } catch (KeeperException.NodeExistsException e) { throw new ConfigManagerRuntimeException.ConfigurationAlreadyExists(path); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void update(String path, String hash, InputStream newBinaryStream) throws OptimisticLockingConfiguration { String tmpPath = processPath("/constellio", path); try { byte[] bytes = IOUtils.toByteArray(newBinaryStream); client.setData().withVersion(Integer.parseInt(hash)).forPath(tmpPath, bytes); } catch (IOException e) { throw new RuntimeException(e); } catch (KeeperException.BadVersionException e) { throw new OptimisticLockingConfiguration(path, hash, ""); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void update(String path, String hash, Document newDocument) throws OptimisticLockingConfiguration { String tmpPath = processPath("/constellio", path); InterProcessMutex lock = lockPath(tmpPath); try { byte[] bytes = getByteFromDocument(newDocument); client.setData().withVersion(Integer.parseInt(hash)).forPath(tmpPath, bytes); unlockPath(lock); } catch (KeeperException.BadVersionException e) { unlockPath(lock); throw new OptimisticLockingConfiguration(path, hash, ""); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void update(String path, String hash, Map<String, String> newProperties) throws OptimisticLockingConfiguration { String tmpPath = processPath("/constellio", path); InterProcessMutex lock = lockPath(tmpPath); try { Properties prop = mapToProperties(newProperties); ByteArrayOutputStream output = new ByteArrayOutputStream(); prop.store(output, null); client.setData().withVersion(Integer.parseInt(hash)).forPath(tmpPath, output.toByteArray()); unlockPath(lock); } catch (IOException e) { unlockPath(lock); throw new RuntimeException(e); } catch (KeeperException.BadVersionException e) { unlockPath(lock); throw new OptimisticLockingConfiguration(path, hash, ""); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void delete(String path) { String tmpPath = processPath(CONFIG_FOLDER, path); InterProcessMutex lock = lockPath(tmpPath); try { client.delete().deletingChildrenIfNeeded().forPath(tmpPath); unlockPath(lock); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void deleteFolder(String path) { throw new UnsupportedOperationException(); } @Override public void delete(String path, String hash) throws OptimisticLockingConfiguration { String tmpPath = processPath(CONFIG_FOLDER, path); InterProcessMutex lock = lockPath(tmpPath); try { client.delete().deletingChildrenIfNeeded().withVersion(Integer.parseInt(hash)).forPath(tmpPath); unlockPath(lock); } catch (BadVersionException e) { unlockPath(lock); throw new ConfigManagerException.OptimisticLockingConfiguration(path, hash, ""); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void deleteAllConfigsIn(String collection) { String tmpPath = processPath(CONFIG_FOLDER, "/" + collection); InterProcessMutex lock = lockPath(tmpPath); try { client.delete().deletingChildrenIfNeeded().forPath(tmpPath); unlockPath(lock); } catch (Exception e) { unlockPath(lock); throw new RuntimeException(e); } } @Override public void copySettingsFrom(File setting) { throw new RuntimeException("Not supported operation copySettingsFrom existing folder"); } @Override public void move(String src, String dest) { throw new RuntimeException("Not supported operation move"); } @Override public void registerListener(String path, ConfigEventListener listener) { String pathTmp = processPath("/constellio", path); if (listener instanceof ConfigUpdatedEventListener) { this.updatedConfigEventListeners.add(pathTmp, (ConfigUpdatedEventListener) listener); } } public Map<String, Object> getCache() { return null; } ////////////////////////////////////////////////////////////////////////////////////////// private String getVersion(String tmpPath) { try { return "" + client.checkExists().forPath(tmpPath).getVersion(); } catch (Exception e) { throw new RuntimeException(e); } } private InterProcessMutex lockPath(String path) { InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, path); InterProcessMutex writeLock = lock.writeLock(); try { writeLock.acquire(); } catch (Exception e) { throw new RuntimeException(e); } return writeLock; } private void unlockPath(InterProcessMutex lock) { try { lock.release(); } catch (Exception e) { throw new RuntimeException(e); } } Document getDocumentFrom(byte[] bytes) { SAXBuilder builder = new SAXBuilder(); try { return builder.build(new ByteArrayInputStream(bytes)); } catch (JDOMException e) { throw new ConfigManagerRuntimeException("JDOM2 Exception", e); } catch (IOException e) { throw new ConfigManagerRuntimeException.CannotCompleteOperation("build Document JDOM2 from file", e); } } byte[] getByteFromDocument(Document document) { XMLOutputter outputter = new XMLOutputter(); String doc = outputter.outputString(document); return doc.getBytes(); } Map<String, String> propertiesToMap(Properties properties) { Map<String, String> mapOfProperties = new HashMap<>(); Enumeration<?> enumeration = properties.propertyNames(); while (enumeration.hasMoreElements()) { String key = (String) enumeration.nextElement(); String value = properties.getProperty(key); mapOfProperties.put(key, value); } return mapOfProperties; } private Properties mapToProperties(Map<String, String> newProperties) { TreeMap<String, String> newPropertiesSorted = new TreeMap<>(newProperties); Properties properties = new Properties(); properties.putAll(newPropertiesSorted); return properties; } private String processPath(String startingPath, String path) { String[] partsTmp = path.split("/"); List<String> parts = new ArrayList<>(); for (String part : partsTmp) { if (!part.equals("")) { parts.add(part); } } if (parts.size() == 1) { startingPath += "/" + parts.get(0); } else if (parts.size() > 1) { int start = 0; if (parts.get(0).contains("constellio")) { start = 1; } for (int i = start; i < parts.size(); i++) { startingPath += "/" + parts.get(i); } } return startingPath; } @Override public void eventReceived(CuratorFramework curatorFramework, CuratorEvent event) throws Exception { switch (event.getType()) { case SET_DATA: { String path = event.getPath(); for (ConfigUpdatedEventListener listener : updatedConfigEventListeners.get(path)) { String pathTmp = processPath("", path); listener.onConfigUpdated(pathTmp); } break; } } } @Override public void close() { } @Override public TextConfiguration getText(String path) { throw new UnsupportedOperationException("TODO"); } }