001/**
002 * Copyright (C) 2014  Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/
003 *
004 * This file is part of Dicoogle/dicoogle.
005 *
006 * Dicoogle/dicoogle is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * Dicoogle/dicoogle is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with Dicoogle.  If not, see <http://www.gnu.org/licenses/>.
018 */
019package pt.ua.dicoogle.plugins.webui;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileReader;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.Reader;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Enumeration;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.Map;
035import java.util.Set;
036import java.util.regex.Pattern;
037import java.util.zip.ZipEntry;
038import java.util.zip.ZipFile;
039import net.sf.json.JSONObject;
040import net.sf.json.JSONSerializer;
041import org.slf4j.LoggerFactory;
042import org.slf4j.Logger;
043
044/** A class type for managing web UI plugins.
045 *
046 * @author Eduardo Pinho <eduardopinho@ua.pt>
047 */
048public class WebUIPluginManager {
049    
050    private static final Logger logger = LoggerFactory.getLogger(WebUIPluginManager.class);
051
052    private static class WebUIEntry {
053        public final WebUIPlugin plugin;
054        public final String directory;
055        public final String zipPath;
056
057        public WebUIEntry(WebUIPlugin plugin, String directory, String zipPath) {
058            assert plugin != null;
059            assert directory != null;
060            this.plugin = plugin;
061            this.directory = directory;
062            this.zipPath = zipPath;
063        }
064        public WebUIEntry(WebUIPlugin plugin, String directory) {
065            this(plugin, directory, null);
066        }
067        public boolean isZipped() {
068            return this.zipPath != null;
069        }
070        public InputStream readFile(String file) throws IOException {
071            if (this.isZipped()) {
072                ZipFile zip = new ZipFile(this.zipPath);
073                return zip.getInputStream(zip.getEntry(this.directory + File.separatorChar + file));
074            } else {
075                File f = new File(this.directory + File.separatorChar + file);
076                return new FileInputStream(f);
077            }
078        }
079
080    }
081    
082    private final Map<String, WebUIEntry> plugins;
083    private final Set<WebUIPlugin> justPlugins;
084    
085    public WebUIPluginManager() {
086        this.plugins = new HashMap<>();
087        this.justPlugins = new HashSet<>();
088    }
089    
090    public void loadAll(File directory) {
091        assert directory != null;
092        if (!directory.exists()) {
093            logger.debug("No web plugins directory, ignoring");
094            return;
095        }
096        if (!directory.isDirectory()) {
097            logger.warn("Can't load web UI plugins, file {} is not a directory", directory);
098            return;
099        }
100        
101        for (File f : directory.listFiles()) {
102            if (!f.isDirectory()) continue;
103            try {
104                WebUIPlugin plugin = this.load(f);
105                logger.info("Loaded web plugin: {}", plugin.getName());
106            } catch (IOException ex) {
107                logger.error("Attempt to load plugin at {} failed", f.getName(), ex);
108            } catch (PluginFormatException ex) {
109                logger.warn("Could not load plugin at {} failed", f.getName(), ex);
110            }
111        }
112    }
113    
114    public WebUIPlugin load(File directory) throws IOException, PluginFormatException {
115        assert directory != null;
116        assert directory.isDirectory();
117        final String dirname = directory.getCanonicalPath();
118        File packageJSON = new File(dirname + File.separatorChar + "package.json");
119        try (BufferedReader reader = new BufferedReader(new FileReader(packageJSON))) {
120            String acc = "";
121            String line;
122            while ((line = reader.readLine()) != null) {
123                acc += line;
124            }
125            WebUIPlugin plugin = WebUIPlugin.fromPackageJSON((JSONObject)JSONSerializer.toJSON(acc));
126            File moduleFile = new File(directory.getAbsolutePath() + File.separatorChar + plugin.getModuleFile());
127            if (!moduleFile.canRead()) {
128                throw new IOException("Module file " + moduleFile.getName() + " cannot be read");
129            }
130            this.plugins.put(plugin.getName(), new WebUIEntry(plugin, dirname));
131            this.justPlugins.add(plugin);
132            return plugin;
133        }
134    }
135
136    /** Load all web plugins from a zip or jar file.
137     * @param pluginZip the zip file containing the plugins
138     * @throws java.io.IOException if the zip file can not be read
139     */
140    public void loadAllFromZip(ZipFile pluginZip) throws IOException {
141        assert pluginZip != null;
142        logger.trace("Discovering web UI plugins in {} ...", pluginZip.getName());
143        Pattern pckDescrMatcher = Pattern.compile("WebPlugins\\" + File.separatorChar + "(\\p{Alnum}|\\-|\\_)+\\" + File.separatorChar + "package.json");
144        Enumeration<? extends ZipEntry> entries = pluginZip.entries();
145        final int DIRNAME_TAIL = "/package.json".length();
146        while (entries.hasMoreElements()) {
147            ZipEntry e = entries.nextElement();
148            if (pckDescrMatcher.matcher(e.getName()).matches()) {
149                String dirname = e.getName().substring(0, e.getName().length() - DIRNAME_TAIL);
150                logger.info("Found web UI plugin in {} at \"{}\"", pluginZip.getName(), dirname);
151                try (BufferedReader reader = new BufferedReader(new InputStreamReader(pluginZip.getInputStream(e)))) {
152                    String acc = "";
153                    String line;
154                    while ((line = reader.readLine()) != null) {
155                        acc += line;
156                    }
157                    WebUIPlugin plugin = WebUIPlugin.fromPackageJSON((JSONObject)JSONSerializer.toJSON(acc));
158                    this.plugins.put(plugin.getName(), new WebUIEntry(plugin, dirname, pluginZip.getName()));
159                    this.justPlugins.add(plugin);
160                } catch (PluginFormatException ex) {
161                    logger.warn("Failed to load plugin at \"{}\": {}", e.getName(), ex.getMessage());
162                }
163            }
164        }
165    }
166    
167    /** Get the descriptor of a web UI plugin.
168     * @param name the name of the plugin
169     * @return a copy of the installed web UI plugin
170     */
171    public WebUIPlugin get(String name) {
172        WebUIPlugin plugin = this.plugins.get(name).plugin;
173        return plugin == null ? null : plugin.copy();
174    }
175    
176    /** Retrieve and return the original JSON object of the plugin.
177     * @param name the name of the plugin
178     * @return a JSON object of the original "package.json"
179     * @throws IOException on error reading "package.json"
180     */
181    public JSONObject retrieveJSON(String name) throws IOException {
182        return retrieveJSON(readFile(name, "package.json"));
183    }
184    
185    /** Retrieve and return the original JSON object of the plugin.
186     * @param reader a reader providing the JSON object
187     * @return a JSON object of the original "package.json"
188     * @throws IOException on error reading "package.json"
189     */
190    private JSONObject retrieveJSON(Reader reader) throws IOException {
191        try (BufferedReader bufreader = new BufferedReader(reader)) {
192            String acc = "";
193            String line;
194            while ((line = bufreader.readLine()) != null) {
195                acc += line;
196            }
197            try {
198                return (JSONObject)JSONSerializer.toJSON(acc);
199            } catch (ClassCastException ex) {
200                throw new IOException("Not a JSON object", ex);
201            }
202        }
203    }
204
205    /** Retrieve and return the original JSON object of the plugin.
206     * @param istream an input stream providing the JSON object
207     * @return a JSON object of the original "package.json"
208     * @throws IOException on error reading "package.json"
209     */
210    private JSONObject retrieveJSON(InputStream istream) throws IOException {
211        return retrieveJSON(new InputStreamReader(istream));
212    }
213
214    public String retrieveModuleJS(String name) throws IOException {
215        String moduleFile = this.plugins.get(name).plugin.getModuleFile();
216        try (BufferedReader reader = new BufferedReader(new InputStreamReader(readFile(name, moduleFile)))) {
217            String acc = "";
218            String line;
219            while ((line = reader.readLine()) != null) {
220                acc += line + '\n';
221            }
222            return acc;
223        }
224    }
225
226    public Collection<WebUIPlugin> pluginSet() {
227        return Collections.unmodifiableSet(justPlugins);
228    }
229
230    public void loadSettings(File settingsFolder) {
231        for (WebUIPlugin plugin : pluginSet()) {
232            try {
233                File pluginSettingsFile = new File(settingsFolder.getPath() + File.separatorChar + plugin.getName() + ".json");
234                if (!pluginSettingsFile.exists()) {
235                    logger.info("Web plugin {} has no settings file", plugin.getName());
236                    continue;
237                }
238                BufferedReader reader = new BufferedReader(new FileReader(pluginSettingsFile));
239                String acc = "";
240                String line;
241                while ((line = reader.readLine()) != null) {
242                    acc += line;
243                }
244                JSONObject settings = (JSONObject) JSONSerializer.toJSON(acc);
245                plugin.setSettings(settings);
246            } catch (IOException ex) {
247                logger.error("Failed to load web plugin settings", ex);
248            }
249        }
250    }
251
252    /** Read a file from an installed plugin's directory.
253     * 
254     * @param pluginName the name of the plugin
255     * @param filename the relative path of the file
256     * @return an input stream with the file's content
257     * @throws IOException if the file does not exist or can not be read
258     */
259    private InputStream readFile(String pluginName, String filename) throws IOException {
260        WebUIEntry e = this.plugins.get(pluginName);
261        if (e == null) {
262            throw new IllegalArgumentException("No such web UI plugin");
263        }
264        return e.readFile(filename);
265    }
266}