/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.core.util.store; import static org.apache.commons.io.FileUtils.moveFileToDirectory; import static org.apache.commons.io.FileUtils.readFileToString; import static org.mule.runtime.api.store.ObjectStoreManager.UNBOUNDED; import static org.mule.runtime.core.util.FileUtils.cleanDirectory; import static org.mule.runtime.core.util.FileUtils.newFile; import org.mule.runtime.api.exception.MuleRuntimeException; import org.mule.runtime.api.i18n.I18nMessage; import org.mule.runtime.api.i18n.I18nMessageFactory; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.serialization.ObjectSerializer; import org.mule.runtime.core.api.store.ExpirableObjectStore; import org.mule.runtime.core.api.store.ListableObjectStore; import org.mule.runtime.api.store.ObjectAlreadyExistsException; import org.mule.runtime.api.store.ObjectDoesNotExistException; import org.mule.runtime.api.store.ObjectStoreException; import org.mule.runtime.api.store.ObjectStoreNotAvailableException; import org.mule.runtime.core.config.i18n.CoreMessages; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.apache.commons.collections.BidiMap; import org.apache.commons.collections.bidimap.TreeBidiMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PersistentObjectStorePartition<T extends Serializable> implements ListableObjectStore<T>, ExpirableObjectStore<T> { private static final String OBJECT_FILE_EXTENSION = ".obj"; private static final String PARTITION_DESCRIPTOR_FILE = "partition-descriptor"; public static final String CORRUPTED_FOLDER = "corrupted-files"; protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private final MuleContext muleContext; private final ObjectSerializer serializer; private boolean loaded = false; private File partitionDirectory; private String partitionName; private final BidiMap realKeyToUUIDIndex = new TreeBidiMap(); PersistentObjectStorePartition(MuleContext muleContext, String partitionName, File partitionDirectory) { this.muleContext = muleContext; serializer = muleContext.getObjectSerializer(); this.partitionName = partitionName; this.partitionDirectory = partitionDirectory; } PersistentObjectStorePartition(MuleContext muleContext, File partitionDirectory) throws ObjectStoreNotAvailableException { this.muleContext = muleContext; serializer = muleContext.getObjectSerializer(); this.partitionDirectory = partitionDirectory; this.partitionName = readPartitionFileName(partitionDirectory); } private String readPartitionFileName(File partitionDirectory) throws ObjectStoreNotAvailableException { File partitionDescriptorFile = new File(partitionDirectory, PARTITION_DESCRIPTOR_FILE); try { return readFileToString(partitionDescriptorFile); } catch (IOException e) { throw new ObjectStoreNotAvailableException(e); } } @Override public synchronized void open() throws ObjectStoreException { createDirectory(partitionDirectory); createOrRetrievePartitionDescriptorFile(); } @Override public void close() throws ObjectStoreException {} @Override public List<Serializable> allKeys() throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { return Collections.unmodifiableList(new ArrayList<Serializable>(realKeyToUUIDIndex.keySet())); } } @Override public boolean contains(Serializable key) throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { return realKeyToUUIDIndex.containsKey(key); } } @Override public void store(Serializable key, T value) throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { if (realKeyToUUIDIndex.containsKey(key)) { throw new ObjectAlreadyExistsException(); } File newFile = createFileToStoreObject(); realKeyToUUIDIndex.put(key, newFile.getName()); serialize(newFile, new StoreValue<T>(key, value)); } } @Override public void clear() throws ObjectStoreException { synchronized (realKeyToUUIDIndex) { try { cleanDirectory(this.partitionDirectory); } catch (IOException e) { throw new ObjectStoreException(I18nMessageFactory.createStaticMessage("Could not clear ObjectStore"), e); } realKeyToUUIDIndex.clear(); } } @Override public T retrieve(Serializable key) throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { if (!realKeyToUUIDIndex.containsKey(key)) { String message = "Key does not exist: " + key; throw new ObjectDoesNotExistException(CoreMessages.createStaticMessage(message)); } String filename = (String) realKeyToUUIDIndex.get(key); File file = getValueFile(filename); return deserialize(file).getValue(); } } @Override public T remove(Serializable key) throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { T value = retrieve(key); deleteStoreFile(getValueFile((String) realKeyToUUIDIndex.get(key))); return value; } } @Override public boolean isPersistent() { return true; } @Override public void expire(long entryTTL, int maxEntries) throws ObjectStoreException { assureLoaded(); synchronized (realKeyToUUIDIndex) { File[] files = listValuesFiles(); Arrays.sort(files, (f1, f2) -> { int result = Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); if (result == 0) { result = f1.getName().compareTo(f2.getName()); } return result; }); int startIndex = trimToMaxSize(files, maxEntries); if (entryTTL == UNBOUNDED) { return; } final long now = System.currentTimeMillis(); for (int i = startIndex; i < files.length; i++) { Long lastModified = files[i].lastModified(); if ((now - lastModified) >= entryTTL) { deleteStoreFile(files[i]); } else { break; } } } } private void assureLoaded() throws ObjectStoreException { if (!loaded) { loadStoredKeysAndFileNames(); } } private void moveToCorruptedFilesFolder(File file) throws IOException { String workingDirectory = (new File(muleContext.getConfiguration().getWorkingDirectory())) .toPath().normalize().toString(); String diffFolder = file.getAbsolutePath().split(workingDirectory)[1]; File corruptedFile = new File(muleContext.getConfiguration().getWorkingDirectory() + File.separator + CORRUPTED_FOLDER + diffFolder); moveFileToDirectory(file, corruptedFile.getParentFile(), true); } private synchronized void loadStoredKeysAndFileNames() throws ObjectStoreException { /* * by re-checking this condition here we can avoid contention in {@link #assureLoaded}. The amount of times that this * condition should evaluate to {@code true} is really limited, which provides better performance in the long run */ if (loaded) { return; } try { File[] files = listValuesFiles(); for (File file : files) { try { StoreValue<T> storeValue = deserialize(file); realKeyToUUIDIndex.put(storeValue.getKey(), file.getName()); } catch (ObjectStoreException e) { if (logger.isWarnEnabled()) { logger.warn(String.format( "Could not deserialize the ObjectStore file: %s. The file will be skipped " + "and moved " + "to the Garbage folder", file.getName())); } moveToCorruptedFilesFolder(file); } } loaded = true; } catch (Exception e) { String message = String.format("Could not restore object store data from %1s", partitionDirectory.getAbsolutePath()); throw new ObjectStoreException(CoreMessages.createStaticMessage(message)); } } private File[] listValuesFiles() { File[] files = partitionDirectory.listFiles((FileFilter) file -> !file.isDirectory() && file.getName().endsWith(OBJECT_FILE_EXTENSION)); if (files == null) { files = new File[0]; } return files; } protected void createDirectory(File directory) throws ObjectStoreException { try { // To support concurrency we need to check if directory exists again // inside // synchronized method if (!directory.exists() && !directory.mkdirs()) { I18nMessage message = CoreMessages.failedToCreate("object store directory " + directory.getAbsolutePath()); throw new MuleRuntimeException(message); } } catch (Exception e) { throw new ObjectStoreException(e); } } private File getValueFile(String filename) { return new File(partitionDirectory, filename); } protected File createFileToStoreObject() throws ObjectStoreException { String filename = org.mule.runtime.core.util.UUID.getUUID() + OBJECT_FILE_EXTENSION; try { return newFile(partitionDirectory, filename); } catch (MuleRuntimeException mre) { throw new ObjectStoreException(mre); } } protected File createOrRetrievePartitionDescriptorFile() throws ObjectStoreException { try { File partitionDescriptorFile = new File(partitionDirectory, PARTITION_DESCRIPTOR_FILE); if (partitionDescriptorFile.exists()) { this.partitionName = readPartitionFileName(partitionDirectory); return partitionDescriptorFile; } FileWriter fileWriter = new FileWriter(partitionDescriptorFile.getAbsolutePath(), false); try { fileWriter.write(partitionName); fileWriter.flush(); } finally { fileWriter.close(); } return partitionDescriptorFile; } catch (Exception e) { throw new ObjectStoreException(e); } } protected void serialize(File outputFile, StoreValue<T> storeValue) throws ObjectStoreException { FileOutputStream out = null; try { out = new FileOutputStream(outputFile); ObjectOutputStream objectOutputStream = new ObjectOutputStream(out); serializer.getInternalProtocol().serialize(storeValue, objectOutputStream); } catch (Exception se) { throw new ObjectStoreException(se); } finally { if (out != null) { try { out.close(); } catch (Exception e) { logger.warn("error closing file " + outputFile.getAbsolutePath()); } } } } @SuppressWarnings("unchecked") protected StoreValue<T> deserialize(File file) throws ObjectStoreException { ObjectInputStream objectInputStream = null; try { objectInputStream = new ObjectInputStream(new FileInputStream(file)); StoreValue<T> storedValue = serializer.getInternalProtocol().deserialize(objectInputStream); if (storedValue.getValue() instanceof DeserializationPostInitialisable) { DeserializationPostInitialisable.Implementation.init(storedValue.getValue(), muleContext); } return storedValue; } catch (FileNotFoundException e) { throw new ObjectDoesNotExistException(e); } catch (Exception e) { throw new ObjectStoreException(e); } finally { if (objectInputStream != null) { try { objectInputStream.close(); } catch (Exception e) { logger.warn("error closing opened file " + file.getAbsolutePath()); } } } } protected void deleteStoreFile(File file) throws ObjectStoreException { if (file.exists()) { if (!file.delete()) { I18nMessage message = CoreMessages.createStaticMessage("Deleting " + file.getAbsolutePath() + " failed"); throw new ObjectStoreException(message); } realKeyToUUIDIndex.removeValue(file.getName()); } else { throw new ObjectDoesNotExistException(); } } private int trimToMaxSize(File[] files, int maxEntries) throws ObjectStoreException { if (maxEntries == UNBOUNDED) { return 0; } int expired = 0; int excess = (files.length - maxEntries); if (excess > 0) { for (int i = 0; i < excess; i++) { deleteStoreFile(files[i]); expired++; } } return expired; } public String getPartitionName() { return partitionName; } public static class StoreValue<T> implements Serializable { private Serializable key; private T value; public StoreValue(Serializable key, T value) { this.key = key; this.value = value; } public Serializable getKey() { return key; } public T getValue() { return value; } } }