Some fileimport enhancements - Add support for Excel import
[simantics/platform.git] / bundles / org.simantics.fileimport / src / org / simantics / fileimport / FileImportService.java
1 package org.simantics.fileimport;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.OutputStream;
6 import java.nio.file.Files;
7 import java.nio.file.Path;
8 import java.nio.file.Paths;
9 import java.util.ArrayList;
10 import java.util.Collections;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Optional;
15 import java.util.Properties;
16 import java.util.Set;
17 import java.util.function.Consumer;
18 import java.util.stream.Collectors;
19
20 import org.osgi.framework.InvalidSyntaxException;
21 import org.osgi.framework.ServiceReference;
22 import org.simantics.databoard.util.Base64;
23 import org.simantics.db.Resource;
24 import org.simantics.fileimport.dropins.FileImportDropins;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 /**
29  * Utility class for Simantics File import functions
30  * 
31  * @author Jani Simomaa
32  *
33  */
34 public class FileImportService {
35
36     private static final Logger LOGGER = LoggerFactory.getLogger(FileImportService.class);
37     
38     private FileImportService() {}
39     
40     public static final String DB_FILE = ".simanticsdb";
41
42     private static List<IGenericFileImport> getFileImportServices() {
43         ServiceReference<?>[] serviceReferences = new ServiceReference<?>[0];
44         try {
45             serviceReferences = Activator.getContext().getAllServiceReferences(IGenericFileImport.class.getName(),
46                     null);
47         } catch (InvalidSyntaxException e) {
48             LOGGER.error("Could not get service references for IGenericFileImport!", e);
49         }
50         if (serviceReferences.length == 0)
51             return Collections.emptyList();
52
53         List<IGenericFileImport> services = new ArrayList<>(serviceReferences.length);
54         for (ServiceReference<?> reference : serviceReferences) {
55             IGenericFileImport service = (IGenericFileImport) Activator.getContext().getService(reference);
56             services.add(service);
57         }
58         return services;
59     }
60
61     /**
62      * Lists all supported file extensions which have a registered service for handling the import
63      * 
64      * @return Map containing the extension and the description of the extension in that order
65      */
66     public static Map<String, String> supportedExtensionsWithFilters() {
67         List<IGenericFileImport> services = getFileImportServices();
68         Map<String, String> extensionsWithFilters = new HashMap<>();
69         for (IGenericFileImport service : services)
70             extensionsWithFilters.putAll(service.allowedExtensionsWithFilters());
71
72         return extensionsWithFilters;
73     }
74     
75     private static class ConsumerHolder implements Consumer<Throwable> {
76
77         private Throwable throwable;
78         
79         @Override
80         public void accept(Throwable t) {
81             throwable = t;
82         }
83         
84         public Throwable getThrowable() {
85             return throwable;
86         }
87         
88     }
89     
90     public static String performFileImport(String base64, String name) throws Throwable {
91         byte[] bytes = Base64.decode(base64);
92         Path file = Activator.getModelsFolder().resolve(name);
93         Files.write(file, bytes);
94         
95         ConsumerHolder holder = new ConsumerHolder();
96         String result = performFileImport(file, Optional.of(holder));
97         if (holder.getThrowable() != null)
98             throw holder.getThrowable();
99         return result;
100     }
101
102     /**
103      * 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
104      * 
105      * @param file Path file to be imported
106      * @param callback Optional callback which can be used to catch Throwables thrown in the import process
107      */
108     public static String performFileImport(Path file, Optional<Consumer<Throwable>> callback) {
109         if (file.getFileName().toString().equals(DB_FILE)) {
110             return null;
111         }
112         String result = "Import failed";
113         IGenericFileImport service = findServiceForFileExtension(file);
114         if (service != null) {
115             try {
116                 Optional<String> resource = service.perform(file);
117                 saveResourceForPath(file, resource);
118                 result = resource.get();
119             } catch (Throwable t) {
120                 if (callback.isPresent()) {
121                     callback.get().accept(t);
122                 } else {
123                     LOGGER.error("Could not import file " + file, t);
124                 }
125             }
126         } else {
127             LOGGER.warn("Could not find service for importing file " + file);
128             if (callback.isPresent())
129                 callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file));
130         }
131         return result;
132     }
133
134     
135     /**
136      * 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
137      * 
138      * @param file Path file that was deleted
139      * @param callback Optional callback to catch Throwables thrown during the deletion process
140      */
141     public static void removeResourceForFile(Path file, Optional<Consumer<Throwable>> callback) {
142         try {
143             Optional<String> resource = getResourceForPath(file);
144             if (!resource.isPresent())
145                 return;
146             IGenericFileImport service = findServiceForFileExtension(file);
147             if (service == null) {
148                 LOGGER.warn("Could not find service for importing file " + file);
149                 if (callback.isPresent())
150                     callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file));
151             }
152             service.remove(resource.get());
153             removeResourceForPath(file);
154         } catch (Throwable t) {
155             if (callback.isPresent()) {
156                 callback.get().accept(t);
157             } else {
158                 LOGGER.error("Could not remove resource for file " + file.toAbsolutePath(), t);
159             }
160         }
161     }
162     
163     public static void removeFileForResource(long id, Optional<Consumer<Throwable>> callback) {
164         Optional<Path> fileOp;
165         try {
166             fileOp = findPathForId(id);
167         } catch (IOException e) {
168             LOGGER.error("Could not remove file for resource id " + id, e);
169             return;
170         }
171         if (!fileOp.isPresent())
172             return;
173         Path file = fileOp.get();
174
175         try {
176             Optional<String> resource = getResourceForPath(file);
177             if (!resource.isPresent())
178                 return;
179             IGenericFileImport service = findServiceForFileExtension(file);
180             if (service == null) {
181                 LOGGER.warn("Could not find service for importing file " + file);
182                 if (callback.isPresent())
183                     callback.get().accept(new Exception("Could not find IGenericFileImport service for file " + file));
184             }
185             service.remove(resource.get());
186             removeResourceForPath(file);
187             try {
188                 Files.delete(file);
189             } catch (IOException e) {
190                 Files.delete(file);
191             }
192         } catch (Throwable t) {
193             if (callback.isPresent()) {
194                 callback.get().accept(t);
195             } else {
196                 LOGGER.error("Could not remove file for resource " + id, t);
197             }
198         }
199     }
200
201     private static Optional<Path> findPathForId(long id) throws IOException {
202         Path db = Activator.getDropinsFolder().resolve(DB_FILE);
203         if (!Files.exists(db))
204             Files.createFile(db);
205         Properties props = new Properties();
206         try (InputStream stream = Files.newInputStream(db)) {
207             props.load(stream);
208         }
209         for (Map.Entry<Object, Object> entry : props.entrySet()) {
210             Long value = Long.valueOf(entry.getValue().toString());
211             if (value.longValue() == id) {
212                 String key = (String) entry.getKey();
213                 return Optional.of(Paths.get(key));
214             }
215         }
216         return Optional.empty();
217     }
218
219     static final String FOLDER = "_folder_";
220     
221     /**
222      * Method for finding a File Import service for the given file based on the file extension
223      * 
224      * @param file Path file for which the import service is looked for
225      * @return Optional IGenerigFileImport service which is able to handle the import of this type of file
226      */
227     public static IGenericFileImport findServiceForFileExtension(Path file) {
228         String extension = "";
229
230         int i = file.getFileName().toString().lastIndexOf('.');
231         if (i > 0) {
232             extension = file.getFileName().toString().substring(i);
233         } else {
234             // Handle case that file is actually a directory
235             if (Files.isDirectory(file) || !Files.isRegularFile(file)) {
236                 extension = FOLDER;
237             }
238         }
239         return findServiceForExtension(extension);
240     }
241
242     public static List<String> filterSupportedExtensions(String filter) {
243         return getFileImportServices().stream().filter(s -> s.allowedExtensionsWithFilters().keySet().contains(filter)).map(s -> s.allowedExtensionsWithFilters().keySet()).flatMap(Set::stream).collect(Collectors.toList());
244     }
245
246     public static IGenericFileImport findServiceForExtension(String extension) {
247         List<IGenericFileImport> services = findServicesForExtension(extension);
248         IGenericFileImport service = null;
249         if (services.size() == 1) {
250             service = services.get(0);
251         } else {
252             for (IGenericFileImport servicee : services) {
253                 service = servicee;
254                 if (isPerfectMatch(servicee.allowedExtensionsWithFilters().keySet(), extension))
255                     break;
256             }
257         }
258         return service;
259     }
260     
261     public static List<IGenericFileImport> findServicesForExtension(String extension) {
262         List<IGenericFileImport> result = new ArrayList<>();
263         List<IGenericFileImport> services = getFileImportServices();
264         for (IGenericFileImport service : services) {
265             for (Map.Entry<String, String> entry : service.allowedExtensionsWithFilters().entrySet()) {
266                 String possibleExtensions = entry.getKey();
267                 if (possibleExtensions.startsWith("*"))
268                     possibleExtensions = possibleExtensions.substring(1);
269                 if (possibleExtensions.equals(extension) || possibleExtensions.isEmpty()) {
270                     if (extension.equals(FOLDER) && possibleExtensions.equals(FOLDER)) {
271                         result.add(service);
272                     } else if (!extension.isEmpty() && !extension.equals(FOLDER)){
273                         result.add(service);
274                     }
275                 }
276             }
277         }
278         return result;
279     }
280     
281     /**
282      * Method for listing all current paths and their corresponding identifiers in Simantics database
283      * 
284      * @return Map containing 
285      */
286     public static Map<String, String> getPathsAndResources() {
287         try {
288             Path db = Activator.getDropinsFolder().resolve(DB_FILE);
289             if (!Files.exists(db))
290                 Files.createFile(db);
291             Properties props = new Properties();
292             try (InputStream stream = Files.newInputStream(db)) {
293                 props.load(stream);
294             }
295             Map<String, String> map = new HashMap<>();
296             for (Map.Entry<Object, Object> entry : props.entrySet()) {
297                 String value = (String) entry.getValue();
298                 String key = (String) entry.getKey();
299                 map.put(key, value);
300             }
301             return map;
302         } catch (IOException e) {
303             LOGGER.error("Could not get current paths and resources!", e);
304             return Collections.emptyMap();
305         }
306     }
307
308     private static void saveResourceForPath(Path file, Optional<String> resource) {
309         resource.ifPresent(res -> {
310             try {
311                 Path db = Activator.getDropinsFolder().resolve(DB_FILE);
312                 if (!Files.exists(db))
313                     Files.createFile(db);
314                 Properties props = new Properties();
315                 try (InputStream stream = Files.newInputStream(db)) {
316                     props.load(stream);
317                 }
318                 props.put(file.getFileName().toString(), resource.get());
319                 try (OutputStream stream = Files.newOutputStream(db)) {
320                     props.store(stream, null);
321                 }
322             } catch (IOException e) {
323                 LOGGER.error("Could not save resource for path " + file.toAbsolutePath() + " and resource " + resource.get(), e);
324             }
325         });
326     }
327
328     private static void removeResourceForPath(Path file) throws IOException {
329         Path db = Activator.getDropinsFolder().resolve(DB_FILE);
330         if (!Files.exists(db))
331             Files.createFile(db);
332         Properties props = new Properties();
333         try (InputStream stream = Files.newInputStream(db)) {
334             props.load(stream);
335         }
336         props.remove(file.getFileName().toString());
337         try (OutputStream stream = Files.newOutputStream(db)) {
338             props.store(stream, null);
339         }
340     }
341     
342     private static Optional<String> getResourceForPath(Path file) throws IOException {
343         Path db = Activator.getDropinsFolder().resolve(DB_FILE);
344         if (!Files.exists(db))
345             Files.createFile(db);
346         Properties props = new Properties();
347         try (InputStream stream = Files.newInputStream(db)) {
348             props.load(stream);
349         }
350         String value = props.getProperty(file.getFileName().toString());
351         if (value == null)
352             return Optional.empty();
353         return Optional.of(value);
354     }
355     
356     public static String importGenericFileWithExtension(String path, String extension) throws Exception {
357         IGenericFileImport service = findServiceForExtension(extension);
358         Optional<String> result = service.perform(Paths.get(path));
359         return result.get();
360     }
361     
362     public static Resource importGenericFileWithExtensionAndParent(Resource parent, String path, String extension) throws Exception {
363         IGenericFileImport service = findServiceForExtension(extension);
364         Optional<Resource> result = service.perform(parent, Paths.get(path));
365         return result.get();
366     }
367
368     private static boolean isPerfectMatch(Set<String> candidates, String extension) {
369         for (String ext : candidates) {
370             if (ext.startsWith("."))
371                 ext = ext.substring(1);
372             if (ext.equals(extension))
373                 return true;
374         }
375         return false;
376     }
377 }