package org.simantics.fileimport; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.simantics.databoard.util.Base64; import org.simantics.db.Resource; import org.simantics.fileimport.dropins.FileImportDropins; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class for Simantics File import functions * * @author Jani Simomaa * */ public class FileImportService { private static final Logger LOGGER = LoggerFactory.getLogger(FileImportService.class); private FileImportService() {} public static final String DB_FILE = ".simanticsdb"; private static List getFileImportServices() { ServiceReference[] serviceReferences = new ServiceReference[0]; try { serviceReferences = Activator.getContext().getAllServiceReferences(IGenericFileImport.class.getName(), null); } catch (InvalidSyntaxException e) { LOGGER.error("Could not get service references for IGenericFileImport!", e); } if (serviceReferences.length == 0) return Collections.emptyList(); List services = new ArrayList<>(serviceReferences.length); for (ServiceReference reference : serviceReferences) { IGenericFileImport service = (IGenericFileImport) Activator.getContext().getService(reference); services.add(service); } return services; } /** * Lists all supported file extensions which have a registered service for handling the import * * @return Map containing the extension and the description of the extension in that order */ public static Map supportedExtensionsWithFilters() { List services = getFileImportServices(); Map extensionsWithFilters = new HashMap<>(); for (IGenericFileImport service : services) extensionsWithFilters.putAll(service.allowedExtensionsWithFilters()); return extensionsWithFilters; } private static class ConsumerHolder implements Consumer { private Throwable throwable; @Override public void accept(Throwable t) { throwable = t; } public Throwable getThrowable() { return throwable; } } public static String performFileImport(String base64, String name) throws Throwable { byte[] bytes = Base64.decode(base64); Path file = Activator.getModelsFolder().resolve(name); Files.write(file, bytes); ConsumerHolder holder = new ConsumerHolder(); String result = performFileImport(file, Optional.empty(), Optional.of(holder)); if (holder.getThrowable() != null) throw holder.getThrowable(); return result; } /** * Method that performs the import of the given file. This method is called when e.g. {@link FileImportDropins} watcher detects {@link java.nio.file.StandardWatchEventKinds.ENTRY_CREATE} operation * * @param file Path file to be imported * @param possibleSelection - the selected resource (if exists) * @param callback Optional callback which can be used to catch Throwables thrown in the import process */ public static String performFileImport(Path file, Optional possibleSelection, Optional> callback) { if (file.getFileName().toString().equals(DB_FILE)) { return null; } String result = "Import failed"; IGenericFileImport service = findServiceForFileExtension(file); if (service != null) { try { Optional resource; if (possibleSelection.isPresent() && service.defaultParentResource() == null) { resource = Optional.of(Long.toString(service.perform(possibleSelection.get(), file).get().getResourceId())); } else { resource = service.performWithDefaultParent(file); } saveResourceForPath(file, resource); result = resource.get(); } catch (Throwable t) { if (callback.isPresent()) { callback.get().accept(t); } else { LOGGER.error("Could not import file " + file, t); } } } else { LOGGER.warn("Could not find service for importing file " + file); if (callback.isPresent()) callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file)); } return result; } /** * Remove the entity that matches the file. This method is called when e.g. the {@link FileImportDropins} watcher detects {@link java.nio.file.StandardWatchEventKinds.ENTRY_DELETE} operation * * @param file Path file that was deleted * @param callback Optional callback to catch Throwables thrown during the deletion process */ public static void removeResourceForFile(Path file, Optional> callback) { try { Optional resource = getResourceForPath(file); if (!resource.isPresent()) return; IGenericFileImport service = findServiceForFileExtension(file); if (service == null) { LOGGER.warn("Could not find service for importing file " + file); if (callback.isPresent()) callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file)); } service.remove(resource.get()); removeResourceForPath(file); } catch (Throwable t) { if (callback.isPresent()) { callback.get().accept(t); } else { LOGGER.error("Could not remove resource for file " + file.toAbsolutePath(), t); } } } public static void removeFileForResource(long id, Optional> callback) { Optional fileOp; try { fileOp = findPathForId(id); } catch (IOException e) { LOGGER.error("Could not remove file for resource id " + id, e); return; } if (!fileOp.isPresent()) return; Path file = fileOp.get(); try { Optional resource = getResourceForPath(file); if (!resource.isPresent()) return; IGenericFileImport service = findServiceForFileExtension(file); if (service == null) { LOGGER.warn("Could not find service for importing file " + file); if (callback.isPresent()) callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file)); } service.remove(resource.get()); removeResourceForPath(file); try { Files.delete(file); } catch (IOException e) { Files.delete(file); } } catch (Throwable t) { if (callback.isPresent()) { callback.get().accept(t); } else { LOGGER.error("Could not remove file for resource " + id, t); } } } private static Optional findPathForId(long id) throws IOException { Path db = Activator.getDropinsFolder().resolve(DB_FILE); if (!Files.exists(db)) Files.createFile(db); Properties props = new Properties(); try (InputStream stream = Files.newInputStream(db)) { props.load(stream); } for (Map.Entry entry : props.entrySet()) { Long value = Long.valueOf(entry.getValue().toString()); if (value.longValue() == id) { String key = (String) entry.getKey(); return Optional.of(Paths.get(key)); } } return Optional.empty(); } static final String FOLDER = "_folder_"; /** * Method for finding a File Import service for the given file based on the file extension * * @param file Path file for which the import service is looked for * @return Optional IGenerigFileImport service which is able to handle the import of this type of file */ public static IGenericFileImport findServiceForFileExtension(Path file) { String extension = ""; int i = file.getFileName().toString().lastIndexOf('.'); if (i > 0) { extension = file.getFileName().toString().substring(i); } else { // Handle case that file is actually a directory if (Files.isDirectory(file) || !Files.isRegularFile(file)) { extension = FOLDER; } } return findServiceForExtension(extension); } public static List filterSupportedExtensions(String filter) { return getFileImportServices().stream().filter(s -> s.allowedExtensionsWithFilters().keySet().contains(filter)).map(s -> s.allowedExtensionsWithFilters().keySet()).flatMap(Set::stream).collect(Collectors.toList()); } public static IGenericFileImport findServiceForExtension(String extension) { List services = findServicesForExtension(extension); IGenericFileImport service = null; if (services.size() == 1) { service = services.get(0); } else { for (IGenericFileImport servicee : services) { service = servicee; if (isPerfectMatch(servicee.allowedExtensionsWithFilters().keySet(), extension)) break; } } return service; } public static List findServicesForExtension(String extension) { List result = new ArrayList<>(); List services = getFileImportServices(); for (IGenericFileImport service : services) { for (Map.Entry entry : service.allowedExtensionsWithFilters().entrySet()) { String possibleExtensions = entry.getKey(); if (possibleExtensions.startsWith("*")) possibleExtensions = possibleExtensions.substring(1); if (possibleExtensions.equals(extension) || possibleExtensions.isEmpty()) { if (extension.equals(FOLDER) && possibleExtensions.equals(FOLDER)) { result.add(service); } else if (!extension.isEmpty() && !extension.equals(FOLDER)){ result.add(service); } } } } return result; } /** * Method for listing all current paths and their corresponding identifiers in Simantics database * * @return Map containing */ public static Map getPathsAndResources() { try { Path db = Activator.getDropinsFolder().resolve(DB_FILE); if (!Files.exists(db)) Files.createFile(db); Properties props = new Properties(); try (InputStream stream = Files.newInputStream(db)) { props.load(stream); } Map map = new HashMap<>(); for (Map.Entry entry : props.entrySet()) { String value = (String) entry.getValue(); String key = (String) entry.getKey(); map.put(key, value); } return map; } catch (IOException e) { LOGGER.error("Could not get current paths and resources!", e); return Collections.emptyMap(); } } private static void saveResourceForPath(Path file, Optional resource) { resource.ifPresent(res -> { try { Path db = Activator.getDropinsFolder().resolve(DB_FILE); if (!Files.exists(db)) Files.createFile(db); Properties props = new Properties(); try (InputStream stream = Files.newInputStream(db)) { props.load(stream); } props.put(file.getFileName().toString(), resource.get()); try (OutputStream stream = Files.newOutputStream(db)) { props.store(stream, null); } } catch (IOException e) { LOGGER.error("Could not save resource for path " + file.toAbsolutePath() + " and resource " + resource.get(), e); } }); } private static void removeResourceForPath(Path file) throws IOException { Path db = Activator.getDropinsFolder().resolve(DB_FILE); if (!Files.exists(db)) Files.createFile(db); Properties props = new Properties(); try (InputStream stream = Files.newInputStream(db)) { props.load(stream); } props.remove(file.getFileName().toString()); try (OutputStream stream = Files.newOutputStream(db)) { props.store(stream, null); } } private static Optional getResourceForPath(Path file) throws IOException { Path db = Activator.getDropinsFolder().resolve(DB_FILE); if (!Files.exists(db)) Files.createFile(db); Properties props = new Properties(); try (InputStream stream = Files.newInputStream(db)) { props.load(stream); } String value = props.getProperty(file.getFileName().toString()); if (value == null) return Optional.empty(); return Optional.of(value); } /** * Calls the proper imported without a selection (null possibleSelection) * @param path * @param extension * @return * @throws Exception */ public static String importGenericFileWithExtension(String path, String extension) throws Exception { IGenericFileImport service = findServiceForExtension(extension); Optional result = service.performWithDefaultParent(Paths.get(path)); return result.get(); } /** * Calls the proper imported without a selection (null possibleSelection) * @param parent * @param path * @param extension * @return * @throws Exception */ public static Resource importGenericFileWithExtensionAndParent(Resource parent, String path, String extension) throws Exception { IGenericFileImport service = findServiceForExtension(extension); Optional result = service.perform(parent, Paths.get(path)); return result.get(); } private static boolean isPerfectMatch(Set candidates, String extension) { for (String ext : candidates) { if (ext.startsWith("*")) ext = ext.substring(1); if (ext.equals(extension)) return true; } return false; } }