| /******************************************************************************* |
| * 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"); |
| } |
| |
| } |