blob: 9006681357db2c0c7987e0cba5b636afffddddf6 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2021 the Eclipse BaSyx Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
******************************************************************************/
package org.eclipse.basyx.vab.modelprovider.filesystem;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.basyx.vab.coder.json.serialization.DefaultTypeFactory;
import org.eclipse.basyx.vab.coder.json.serialization.GSONTools;
import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
import org.eclipse.basyx.vab.exception.provider.ProviderException;
import org.eclipse.basyx.vab.exception.provider.ResourceAlreadyExistsException;
import org.eclipse.basyx.vab.exception.provider.ResourceNotFoundException;
import org.eclipse.basyx.vab.modelprovider.VABPathTools;
import org.eclipse.basyx.vab.modelprovider.api.IModelProvider;
import org.eclipse.basyx.vab.modelprovider.filesystem.filesystem.File;
import org.eclipse.basyx.vab.modelprovider.filesystem.filesystem.FileSystem;
import org.eclipse.basyx.vab.modelprovider.filesystem.filesystem.FileType;
/**
* Provides models based on a generic file system
*
* @author schnicke, elsheikh, conradi
*
*/
public class FileSystemProvider implements IModelProvider {
private final FileSystem fileSystem;
private final String rootDir;
private final String collectionElemPrefix = "byRef_";
private final String metaFileName = "_meta";
private final String referenceFileName = "references";
private final String regexCollectionElem = collectionElemPrefix + "(0|[1-9][0-9]*)";
private final GSONTools tools = new GSONTools(new DefaultTypeFactory());
/**
* Constructor which takes a file system and a root directory
* Removes the last '/' from the passed root directory if it exists
* Creates the root directory folder
*/
public FileSystemProvider(FileSystem fileSystem, String rootDir) throws ProviderException {
this.fileSystem = fileSystem;
this.rootDir = unifyPath(rootDir);
createDirectory(rootDir + "/");
}
public FileSystemProvider(FileSystem fileSystem, String rootDir, Map<String, Object> VABelement) throws ProviderException {
this.fileSystem = fileSystem;
this.rootDir = unifyPath(rootDir);
createDirectory(rootDir + "/");
fromMapToDirectory("", VABelement);
}
/**
* Same constructor as the above one, only gets an additional boolean argument
* doEmptyDirectory which specifies whether to empty the root directory or not
*/
public FileSystemProvider(FileSystem fileSystem, String rootDir, Map<String, Object> VABelement,
boolean doEmptyDirectory) throws ProviderException {
this.fileSystem = fileSystem;
this.rootDir = unifyPath(rootDir);
createDirectory(rootDir + "/");
if (doEmptyDirectory)
deleteDirectory(rootDir);
fromMapToDirectory("", VABelement);
}
/**
* Removes the first and last character from a String if it is a "/"
* @throws MalformedRequestException
*/
private String unifyPath(String path) throws MalformedRequestException {
VABPathTools.checkPathForNull(path);
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
return path;
}
private String constructCollectionRefPath(String path, int ref) {
return path + "/" + collectionElemPrefix + ref;
}
/**
* Reads the __meta file present in the specified directory. Returns null if the
* __meta file does not exist. Works only without "/" at the end
*/
@SuppressWarnings("unchecked")
private HashSet<String> readMetaFile(String path) throws ProviderException {
path = path.equals("") ? rootDir + "/" + metaFileName : rootDir + "/" + path + "/" + metaFileName;
if (fileSystem.getType(path) == FileType.DATA) {
Object deserialized = loadAndDeserialize(path);
// Especially for "[]", deserialization can not differentiate between lists and sets
if (deserialized instanceof HashSet) {
return (HashSet<String>) deserialized;
} else if (deserialized instanceof Collection) {
return new HashSet<>((List<String>) deserialized);
}
}
return null;
}
private File findFileInList(List<File> files, String fileName) {
if (fileName.equals("")) {
// The wanted File is the root
return new File(rootDir, FileType.DIRECTORY);
}
for (File file : files) {
String currentFileName = VABPathTools.getLastElement(file.getName());
if (currentFileName.equals(fileName)) {
return file;
}
}
return null;
}
private Collection<Object> readCollection(String path) throws ProviderException {
Collection<Object> c = new ArrayList<Object>();
String fullPath = rootDir + "/" + path;
for (int ref : readReferences(fullPath)) {
FileType type = fileSystem.getType(constructCollectionRefPath(fullPath, ref));
if (type == FileType.DATA) {
c.add(loadAndDeserialize(constructCollectionRefPath(fullPath, ref)));
} else if (type == FileType.DIRECTORY) {
c.add(readDirectory(constructCollectionRefPath(path, ref)));
}
}
return c;
}
/**
* Reads the folder in the relative path specified
*/
private HashMap<String, Object> readDirectory(String path) throws ProviderException {
String fullPath = rootDir + "/" + path;
HashMap<String, Object> returnData = new HashMap<String, Object>();
HashSet<String> collections = readMetaFile(path);
List<File> directoryFiles;
try {
directoryFiles = fileSystem.readDirectory(fullPath);
} catch (IOException e) {
throw new ProviderException("Path \"" + path + "\" could not be read.");
}
removeMetaFile(directoryFiles);
for (File file : directoryFiles) {
String currentFilePath = file.getName();
String fileName = VABPathTools.getLastElement(currentFilePath);
if (file.getType() == FileType.DATA) {
// It's a file
returnData.put(fileName, loadAndDeserialize(currentFilePath));
} else if (collections != null && collections.contains(fileName)) {
// It's a collection
returnData.put(fileName, readCollection(stripRootDir(currentFilePath)));
} else {
// It's a folder
returnData.put(fileName, readDirectory(stripRootDir(currentFilePath)));
}
}
return returnData;
}
private List<File> removeMetaFile(List<File> list) {
for (int i = 0; i < list.size(); i++) {
if (VABPathTools.getLastElement(list.get(i).getName()).equals(metaFileName)) {
list.remove(i);
break;
}
}
return list;
}
private String stripRootDir(String path) {
return path.substring(rootDir.length() + 1);
}
/**
* Adds collection to the __meta file present in directoryPath
* Works whether "/" is at the end of path or not
*/
private void addCollectionToMetaFile(String directoryPath, String collectionName) throws ProviderException {
HashSet<String> collections = readMetaFile(directoryPath);
if (collections != null) {
collections.add(collectionName);
} else {
collections = new HashSet<String>(Arrays.asList(new String[] { collectionName }));
}
serializeAndSave(rootDir + "/" + directoryPath + "/" + metaFileName, collections);
}
/**
* Serializes and writes the object in the path specified. The path is relative to the RootDir
* If the write folder is not present, it is created
* If the object is an array or a collection, a collection is written in a folder
*/
@SuppressWarnings("unchecked")
private void writeObject(String path, Object o) throws ProviderException {
path = unifyPath(path);
String directory = VABPathTools.getParentPath(path);
String fullPath = rootDir + "/" + path;
Collection<?> collection = null;
if (o instanceof Collection<?>) {
collection = (Collection<?>) o;
}
if (collection != null) {
// It's a collection given as an Array or Collection instance
addCollectionToMetaFile(directory, VABPathTools.getLastElement(path));
createDirectory(fullPath);
Iterator<?> iterator = collection.iterator();
List<Integer> references = new ArrayList<>();
for (int counter = 0; iterator.hasNext(); counter++) {
Object item = iterator.next();
references.add(counter);
if (item instanceof Map) {
createDirectory(constructCollectionRefPath(fullPath, counter));
fromMapToDirectory(constructCollectionRefPath(path, counter), (Map<String, Object>) item);
} else {
serializeAndSave(constructCollectionRefPath(fullPath, counter), item);
}
}
writeReferences(fullPath, references);
} else {
// Otherwise, it's an Object
createDirectory(rootDir + "/" + directory);
serializeAndSave(fullPath, o);
}
}
private void createDirectory(String path) throws ProviderException {
try {
fileSystem.createDirectory(path);
} catch (IOException e) {
throw new ProviderException("Directory \"" + path + "\" could not be created.");
}
}
private void deleteDirectory(String path) throws ProviderException {
try {
fileSystem.deleteDirectory(path);
} catch (IOException e) {
throw new ProviderException("Directory \"" + path + "\" could not be deleted.");
}
}
private void deleteFile(String path) throws ProviderException {
try {
fileSystem.deleteFile(path);
} catch (IOException e) {
throw new ProviderException("File \"" + path + "\" could not be deleted.");
}
}
/**
* Mirrors a Map<String, Object> folder structure in the specified relative path
* Works whether "/" is at the end of path or not
* Does not create the directory "path"
*/
@SuppressWarnings("unchecked")
private void fromMapToDirectory(String path, Map<String, Object> map) throws ProviderException {
path = unifyPath(path);
String fullPath = rootDir + "/" + path;
createDirectory(fullPath);
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Map)
fromMapToDirectory(path + "/" + entry.getKey(), (Map<String, Object>) entry.getValue());
else
writeObject(path + "/" + entry.getKey(), entry.getValue());
}
}
private void writeReferences(String path, List<Integer> ref) throws ProviderException {
serializeAndSave(path + "/" + referenceFileName, ref);
}
@SuppressWarnings("unchecked")
private List<Integer> readReferences(String path) throws ProviderException {
return (List<Integer>) loadAndDeserialize(path + "/" + referenceFileName);
}
private Object loadAndDeserialize(String path) throws ProviderException {
try {
String serialized = fileSystem.readFile(path);
return tools.deserialize(serialized);
} catch (NoSuchFileException e) {
throw new ResourceNotFoundException("File \"" + path + "\" does not exist.");
} catch (IOException e) {
throw new ProviderException("File \"" + path + "\" could not be read.");
}
}
private void serializeAndSave(String path, Object o) throws ProviderException {
try {
fileSystem.writeFile(path, tools.serialize(o));
} catch (IOException e) {
throw new ProviderException("File \"" + path + "\" could not be written.");
}
}
@Override
public synchronized Object getValue(String path) throws ProviderException {
path = unifyPath(path);
String directory = VABPathTools.getParentPath(path);
String fileName = VABPathTools.getLastElement(path);
String fullDirPath = rootDir + "/" + directory;
if (fileSystem.getType(fullDirPath) == FileType.DIRECTORY) {
List<File> directoryFiles;
try {
directoryFiles = fileSystem.readDirectory(fullDirPath);
} catch (IOException e) {
throw new MalformedRequestException("Given directory \"" + directory + "\" could not be read.");
}
// The directory that contains the file, folder or collection to be read does
// not exist, return null
// Get the list of collections that are present as folders from the _meta file
HashSet<String> collections = readMetaFile(directory);
if (collections != null && collections.contains(fileName)) {
// It's a collection
return readCollection(path);
} else {
File file = findFileInList(directoryFiles, fileName);
if (file != null) {
if (file.getType() == FileType.DATA) {
// It's a file
return loadAndDeserialize(file.getName());
} else {
// It's a folder
return readDirectory(path);
}
} else if (fileName.matches(regexCollectionElem)) {
// We wanted to read an element of a collection. The element does not exist,
// throw an Invalid List Reference Exception
throw new ResourceNotFoundException("The specified list element \"" +
fileName.substring(collectionElemPrefix.length()) + "\" does not exist.");
}
}
}
throw new ResourceNotFoundException("The specified element \"" + path + "\" does not exist.");
}
/**
* Sets the file, folder, or collection at the specified path to newValue
* Only works if the types match (i.e. file ↔ file, folder ↔ folder, etc...)
*/
@Override
@SuppressWarnings("unchecked")
public synchronized void setValue(String path, Object newValue) throws ProviderException {
path = unifyPath(path);
String fileName = VABPathTools.getLastElement(path);
String fullPath = rootDir + "/" + path;
HashSet<String> collections = readMetaFile(VABPathTools.getParentPath(path));
FileType type = fileSystem.getType(fullPath);
if (type == FileType.DATA) {
// File with the same name exists, replace it with newValue >IF< newValue is
// neither a folder nor a Map
if (!(newValue instanceof Map) && !(newValue instanceof Collection<?>)) {
serializeAndSave(fullPath, newValue);
} else {
throw new MalformedRequestException("The single value at \"" + path +
"\" can not be replaced with a Map or Collection");
}
} else if (type == FileType.DIRECTORY) {
if ((collections == null || !collections.contains(fileName)) && newValue instanceof Map) {
deleteDirectory(fullPath);
fromMapToDirectory(path, (Map<String, Object>) newValue);
} else {
deleteDirectory(fullPath);
writeObject(path, newValue);
}
} else {
throw new ResourceNotFoundException("Value \"" + path + "\" does not exist.");
}
}
/**
* Creates newEntity at the specified path
* If a collection exists at the specified path, add newEntity to it >IF< newEntity is not a collection
*/
@Override
@SuppressWarnings("unchecked")
public synchronized void createValue(String path, Object newEntity) throws ProviderException {
path = unifyPath(path);
String parentPath = VABPathTools.getParentPath(path);
String fileName = VABPathTools.getLastElement(path);
if(fileSystem.getType(rootDir + "/" + parentPath) == null) {
throw new ResourceNotFoundException("Parent-path for \"" + path + "\" does not exist.");
}
String fullPath = rootDir + "/" + path;
FileType type = fileSystem.getType(fullPath);
if (type == FileType.DATA) {
// If it would have been a list, entries could have been added
// => invalid type
throw new ResourceAlreadyExistsException("Could not create a value for \"" + path + "\" because a value already exists");
} else if (type == FileType.DIRECTORY) {
HashSet<String> collections = readMetaFile(parentPath);
if (collections != null && collections.contains(fileName)) {
// the given path is a folder and it contains a collection
List<Integer> references = readReferences(fullPath);
// Get maximum reference to be able to add an additional entry
int max = 0;
for (Integer i : references) {
if (i > max) {
max = i;
}
}
// Add the entry to the collection
if (newEntity instanceof Map) {
fromMapToDirectory(constructCollectionRefPath(path, max + 1), (Map<String, Object>) newEntity);
} else if (!(newEntity instanceof Collection<?>)) {
// If the new Object is a Collection, don't add it to the existing one
serializeAndSave(constructCollectionRefPath(fullPath, max + 1), newEntity);
} else {
throw new MalformedRequestException("The given newEntity is a Collection "
+ "and can therefore not be added to the existing Collection \"" + path + "\".");
}
references.add(max + 1);
writeReferences(fullPath, references);
} else {
// at given path exists a folder, but it is a map.
throw new ResourceAlreadyExistsException("At given path \"" + path + "\" exists a Map.");
}
} else if (type == null) {
// The Object doesn't exist and can be created
if (newEntity instanceof Map) {
fromMapToDirectory(path, (Map<String, Object>) newEntity);
} else {
writeObject(path, newEntity);
}
}
}
/**
* Deletes the Object, folder or collection at the specified path
* If it is a collection, remove its name from the meta file of
* the folder that contains the collection
*/
@Override
public synchronized void deleteValue(String path) throws ProviderException {
path = unifyPath(path);
String directory = VABPathTools.getParentPath(path);
String fileName = VABPathTools.getLastElement(path);
String fullDirPath = rootDir + "/" + directory;
String fullPath = rootDir + "/" + path;
HashSet<String> collections = readMetaFile(directory);
FileType type = fileSystem.getType(fullPath);
if (type == FileType.DATA) {
deleteFile(fullPath);
if (fileName.matches(regexCollectionElem)) {
// The deleted file was an element of a collection (It is named "byRef_*")
int deletedElementIndex = Integer.parseInt(fileName.substring(collectionElemPrefix.length()));
List<Integer> references = readReferences(fullDirPath);
references.remove(Integer.valueOf(deletedElementIndex));
writeReferences(fullDirPath, references);
}
return;
} else if (type == FileType.DIRECTORY) {
if (collections != null && collections.contains(fileName)) {
// The folder to delete is a collection
collections.remove(fileName);
serializeAndSave(fullDirPath + "/" + metaFileName, collections);
}
deleteDirectory(fullPath);
return;
}
throw new ResourceNotFoundException("Value \"" + path + "\" can not be deleted as it does not exist.");
}
/**
* Deletes the Object or Map that is equal to obj from the collection
* in the specified path
* Otherwise, throw a ResourceNotFoundException
*/
@Override
public void deleteValue(String path, Object obj) throws ProviderException {
path = unifyPath(path);
String directory = VABPathTools.getParentPath(path);
String fileName = VABPathTools.getLastElement(path);
String fullCollectionPath = rootDir + "/" + path;
HashSet<String> collections = readMetaFile(directory);
FileType type = fileSystem.getType(rootDir + "/" + directory);
if (collections != null && type == FileType.DIRECTORY && collections.contains(fileName)) {
// Collection in specified path exists
List<Integer> references = readReferences(fullCollectionPath);
for (int i = 0; i < references.size(); i++) {
int j = references.get(i);
String currentPath = constructCollectionRefPath(fullCollectionPath, j);
type = fileSystem.getType(currentPath);
// If the File exists it's a Java Object, else it's a Map
if (type == FileType.DATA) {
Object o = loadAndDeserialize(currentPath);
if (o.equals(obj)) {
deleteFile(currentPath);
references.remove(Integer.valueOf(j));
writeReferences(fullCollectionPath, references);
return;
}
} else if (type == FileType.DIRECTORY) {
Object o = readDirectory(constructCollectionRefPath(path, j));
if (o.equals(obj)) {
deleteDirectory(currentPath);
references.remove(Integer.valueOf(j));
writeReferences(fullCollectionPath, references);
return;
}
}
}
throw new ResourceNotFoundException("Specified Object was not found in Collection \"" + path + "\".");
} else {
// The Collection in given path does not exist
throw new MalformedRequestException("No Collection found at path \"" + path + "\". Delete by value is only possible in Collections.");
}
}
@Override
public Object invokeOperation(String path, Object... parameter) throws ProviderException {
throw new MalformedRequestException("Invoke not supported by filesystem");
}
}