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.server.web;
020
021import org.apache.commons.codec.digest.Md5Crypt;
022import pt.ua.dicoogle.plugins.PluginController;
023import pt.ua.dicoogle.plugins.webui.WebUIPlugin;
024import pt.ua.dicoogle.server.web.rest.VersionResource;
025import pt.ua.dicoogle.server.web.servlets.RestletHttpServlet;
026import pt.ua.dicoogle.server.web.servlets.ExportToCSVServlet;
027import pt.ua.dicoogle.server.web.servlets.SettingsServlet;
028import pt.ua.dicoogle.server.web.servlets.TagsServlet;
029import pt.ua.dicoogle.server.web.servlets.ExportCSVToFILEServlet;
030import pt.ua.dicoogle.server.web.servlets.SearchHolderServlet;
031import pt.ua.dicoogle.server.web.servlets.IndexerServlet;
032import pt.ua.dicoogle.server.web.servlets.ImageServlet;
033import pt.ua.dicoogle.server.web.servlets.search.ExportServlet;
034import pt.ua.dicoogle.server.web.servlets.search.ExportServlet.ExportType;
035import pt.ua.dicoogle.server.web.servlets.search.ProvidersServlet;
036import pt.ua.dicoogle.server.web.servlets.search.SearchServlet;
037import pt.ua.dicoogle.server.web.servlets.search.SearchServlet.SearchType;
038import pt.ua.dicoogle.server.web.servlets.search.WadoServlet;
039import pt.ua.dicoogle.server.web.servlets.accounts.LoginServlet;
040import pt.ua.dicoogle.server.web.servlets.accounts.UserServlet;
041import pt.ua.dicoogle.core.ServerSettings;
042import pt.ua.dicoogle.server.web.servlets.management.AETitleServlet;
043import pt.ua.dicoogle.server.web.servlets.management.DicomQuerySettingsServlet;
044import pt.ua.dicoogle.server.web.servlets.management.ForceIndexing;
045import pt.ua.dicoogle.server.web.servlets.management.IndexerSettingsServlet;
046import pt.ua.dicoogle.server.web.servlets.management.LoggerServlet;
047import pt.ua.dicoogle.server.web.servlets.management.RemoveServlet;
048import pt.ua.dicoogle.server.web.servlets.management.RunningTasksServlet;
049import pt.ua.dicoogle.server.web.servlets.management.ServerStorageServlet;
050import pt.ua.dicoogle.server.web.servlets.management.ServicesServlet;
051import pt.ua.dicoogle.server.web.servlets.management.TransferOptionsServlet;
052
053import java.net.URL;
054import java.nio.file.Files;
055import java.nio.file.Path;
056import java.util.EnumSet;
057
058import org.eclipse.jetty.server.Handler;
059import org.eclipse.jetty.server.Server;
060import org.eclipse.jetty.server.handler.ContextHandlerCollection;
061import org.eclipse.jetty.servlet.ServletContextHandler;
062import org.eclipse.jetty.servlet.ServletHolder;
063import org.eclipse.jetty.webapp.WebAppContext;
064
065import javax.servlet.DispatcherType;
066import javax.servlet.http.HttpServlet;
067import javax.servlet.http.HttpServletRequest;
068
069import org.eclipse.jetty.server.handler.HandlerCollection;
070import org.eclipse.jetty.server.handler.HandlerList;
071import org.eclipse.jetty.server.handler.HandlerWrapper;
072
073import org.eclipse.jetty.servlet.FilterHolder;
074import org.eclipse.jetty.servlets.GzipFilter;
075import org.slf4j.Logger;
076import org.slf4j.LoggerFactory;
077
078import pt.ua.dicoogle.server.LegacyRestletApplication;
079import pt.ua.dicoogle.server.web.servlets.accounts.LogoutServlet;
080import pt.ua.dicoogle.server.web.servlets.management.UnindexServlet;
081import pt.ua.dicoogle.server.web.servlets.search.DumpServlet;
082import pt.ua.dicoogle.server.web.servlets.webui.WebUIModuleServlet;
083import pt.ua.dicoogle.server.web.servlets.webui.WebUIServlet;
084import pt.ua.dicoogle.server.web.utils.LocalImageCache;
085import pt.ua.dicoogle.server.PluginRestletApplication;
086import pt.ua.dicoogle.server.web.utils.SimpleImageRetriever;
087
088/**
089 * @author António Novo <antonio.novo@ua.pt>
090 * @author Luís A. Bastião Silva <bastiao@ua.pt>
091 * @author Frederico Valente
092 * @author Frederico Silva <fredericosilva@ua.pt>
093 * @author Eduardo Pinho <eduardopinho@ua.pt>
094 */
095public class DicoogleWeb {
096
097    private static final Logger logger = LoggerFactory.getLogger(DicoogleWeb.class);
098    /**
099     * Sets the path where the web-pages/scripts or .war are.
100     */
101    public static final String WEBAPPDIR = "webapp";
102
103    /**
104     * Sets the context path used to serve the contents.
105     */
106    public static final String CONTEXTPATH = "/";
107    private LocalImageCache cache = null;
108    private Server server = null;
109    private final int port;
110
111    private final ContextHandlerCollection contextHandlers;
112    private ServletContextHandler pluginHandler = null;
113    private PluginRestletApplication pluginApp = null;
114    private ServletContextHandler legacyHandler = null;
115    private LegacyRestletApplication legacyApp = null;
116
117    /**
118     * Initializes and starts the Dicoogle Web service.
119     * @param port the server port
120     * @throws java.lang.Exception
121     */
122    public DicoogleWeb(int port) throws Exception {
123        logger.info("Starting Web Services in DicoogleWeb. Port: {}", port);
124        System.setProperty("org.apache.jasper.compiler.disablejsr199", "true");
125        //System.setProperty("org.mortbay.jetty.webapp.parentLoaderPriority", "true");
126        //System.setProperty("production.mode", "true");
127
128        this.port = port;
129
130        // "build" the input location, based on the www directory/.war chosen
131        final URL warUrl = Thread.currentThread().getContextClassLoader().getResource(WEBAPPDIR);
132        final String warUrlString = warUrl.toExternalForm();
133
134        //setup the DICOM to PNG image servlet, with a local cache
135        cache = new LocalImageCache("dic2png", 300, 900, new SimpleImageRetriever()); // pooling rate of 12/hr and max un-used cache age of 15 minutes
136        final ServletContextHandler dic2png = createServletHandler(new ImageServlet(cache), "/dic2png");
137        cache.start(); // start the caching system
138
139        // setup the DICOM to PNG image servlet
140        final ServletContextHandler dictags = createServletHandler(new TagsServlet(), "/dictags");
141
142        // setup the Export to CSV Servlet
143        final ServletContextHandler csvServletHolder = createServletHandler(new ExportToCSVServlet(), "/export");
144        Path tempDir = Files.createTempDirectory("dicoogle");
145        csvServletHolder.addServlet(new ServletHolder(new ExportCSVToFILEServlet(tempDir)), "/exportFile");
146
147        // setup the search (DIMSE-service-user C-FIND ?!?) servlet
148        //final ServletContextHandler search = new ServletContextHandler(ServletContextHandler.SESSIONS); // servlet with session support enabled
149        //search.setContextPath(CONTEXTPATH);
150        //search.addServlet(new ServletHolder(new SearchServlet()), "/search");
151
152        /*hooks = getRegisteredHookActions();
153         plugin.addServlet(new ServletHolder(new PluginsServlet(hooks)), "/plugin/*");
154         */
155
156        // setup the web pages/scripts app
157        final WebAppContext webpages = new WebAppContext(warUrlString, CONTEXTPATH);
158        webpages.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); // disables directory listing
159        webpages.setInitParameter("useFileMappedBuffer", "false");
160        webpages.setInitParameter("cacheControl", "public, max-age=2592000"); // cache for 30 days
161        webpages.setInitParameter("etags", "true"); // generate and handle weak entity validation tags
162        webpages.setDisplayName("webapp");
163        webpages.setWelcomeFiles(new String[]{"index.html"});
164        webpages.addServlet(new ServletHolder(new SearchHolderServlet()), "/search/holders");
165        webpages.addFilter(GzipFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
166        
167        this.pluginApp = new PluginRestletApplication();
168        this.pluginHandler = createServletHandler(new RestletHttpServlet(this.pluginApp), "/ext/*");
169
170        this.legacyApp = new LegacyRestletApplication();
171        this.legacyHandler = createServletHandler(new RestletHttpServlet(this.legacyApp), "/legacy/*");
172        
173        // Add Static RESTlet Plugins
174        PluginRestletApplication.attachRestPlugin(new VersionResource());
175        
176        // list the all the handlers mounted above
177        Handler[] handlers = new Handler[]{
178            pluginHandler,
179            legacyHandler,
180            dic2png,
181            dictags,
182            createServletHandler(new IndexerServlet(), "/indexer"), // DEPRECATED
183            createServletHandler(new SettingsServlet(), "/settings"),
184            csvServletHolder,
185            createServletHandler(new LoginServlet(), "/login"),
186            createServletHandler(new LogoutServlet(), "/logout"),
187            createServletHandler(new UserServlet(), "/user"),
188            createServletHandler(new SearchServlet(), "/search"),
189            createServletHandler(new SearchServlet(SearchType.PATIENT), "/searchDIM"),
190            createServletHandler(new DumpServlet(), "/dump"),
191            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.path) , "/management/settings/index/path"),
192            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.zip), "/management/settings/index/zip"),
193            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.effort), "/management/settings/index/effort"),
194            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.thumbnail), "/management/settings/index/thumbnail"),
195            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.watcher), "/management/settings/index/watcher"),
196            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.thumbnailSize), "/management/settings/index/thumbnail/size"),
197            createServletHandler(new IndexerSettingsServlet(IndexerSettingsServlet.SettingsType.all), "/management/settings/index"),
198            createServletHandler(new TransferOptionsServlet(), "/management/settings/transfer"),
199            createServletHandler(new WadoServlet(), "/wado"),
200            createServletHandler(new ProvidersServlet(), "/providers"),
201            createServletHandler(new DicomQuerySettingsServlet(), "/management/settings/dicom/query"),
202            createServletHandler(new ForceIndexing(), "/management/tasks/index"),
203            createServletHandler(new UnindexServlet(), "/management/tasks/unindex"),
204            createServletHandler(new RemoveServlet(), "/management/tasks/remove"),
205            createServletHandler(new ServicesServlet(ServicesServlet.STORAGE), "/management/dicom/storage"),
206            createServletHandler(new ServicesServlet(ServicesServlet.QUERY), "/management/dicom/query"),
207            createServletHandler(new ServicesServlet(ServicesServlet.PLUGIN), "/management/plugins/"),
208            createServletHandler(new AETitleServlet(), "/management/settings/dicom"),
209            createServletHandler(new WebUIServlet(), "/webui"),
210            createWebUIModuleServletHandler(),
211            createServletHandler(new LoggerServlet(), "/logger"),
212            createServletHandler(new RunningTasksServlet(), "/index/task"),
213            createServletHandler(new ExportServlet(ExportType.EXPORT_CVS), "/export/cvs"),
214            createServletHandler(new ExportServlet(ExportType.LIST), "/export/list"),
215            createServletHandler(new ServerStorageServlet(), "/management/settings/storage/dicom"),
216            webpages
217        };
218
219        // setup the server
220        server = new Server(port);
221        // register the handlers on the server
222        this.contextHandlers = new ContextHandlerCollection();
223        this.contextHandlers.setHandlers(handlers);
224        server.setHandler(this.contextHandlers);
225        
226        // and then start the server
227        server.start();
228    }
229
230    private ServletContextHandler createWebUIModuleServletHandler() {
231        ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS); // servlet with session support enabled
232        handler.setContextPath(CONTEXTPATH);
233
234        HttpServlet servletModule = new WebUIModuleServlet();
235        // CORS support
236        this.addCORSFilter(handler);
237        // Caching!
238        FilterHolder cacheHolder = new FilterHolder(new AbstractCacheFilter() {
239            @Override
240            protected String etag(HttpServletRequest req) {
241                String name = req.getRequestURI().substring("/webui/module/".length());
242                WebUIPlugin plugin = PluginController.getInstance().getWebUIPlugin(name);
243                if (plugin == null) return null;
244                if (WebUIModuleServlet.isPrerelease(plugin.getVersion())) {
245                    // pre-release, use hash (to facilitate development)
246                    String fingerprint = PluginController.getInstance().getWebUIModuleJS(name);
247                    return '"' + Md5Crypt.md5Crypt(fingerprint.getBytes()) + '"';
248                } else {
249                    // normal release, use weak ETag
250                    String pProcess = req.getParameter("process");
251                    boolean process = pProcess == null || Boolean.parseBoolean(pProcess);
252                    if (process) {
253                        return "W/\"" + plugin.getName() + '@' + plugin.getVersion() + '"';
254                    } else {
255                        return "W/\"" + plugin.getName() + '@' + plugin.getVersion() + ";raw\"";
256                    }
257                }
258            }
259        });
260        cacheHolder.setInitParameter(AbstractCacheFilter.CACHE_CONTROL_PARAM, "private, max-age=2592000"); // cache for 30 days
261        handler.addFilter(cacheHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
262        handler.addServlet(new ServletHolder(servletModule), "/webui/module/*");
263        return handler;
264    }
265
266    private ServletContextHandler createServletHandler(HttpServlet servlet, String path){
267        ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS); // servlet with session support enabled
268        handler.setContextPath(CONTEXTPATH);
269        
270        // CORS support
271        this.addCORSFilter(handler);
272
273        handler.addServlet(new ServletHolder(servlet), path);
274        return handler;
275    }
276
277    private void addCORSFilter(ServletContextHandler handler) {
278        String origins = ServerSettings.getInstance().getWeb().getAllowedOrigins();
279        if (origins != null) {
280            handler.setDisplayName("cross-origin");
281            FilterHolder corsHolder = new FilterHolder(CORSFilter.class);
282            corsHolder.setInitParameter(CORSFilter.ALLOWED_ORIGINS_PARAM, origins);
283            corsHolder.setInitParameter(CORSFilter.ALLOWED_METHODS_PARAM, "GET,POST,HEAD,PUT,DELETE");
284            corsHolder.setInitParameter(CORSFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin,Authorization,Content-Length");
285            handler.addFilter(corsHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
286        }
287    }
288
289    private void addCORSFilter(Handler handler) {
290        String origins = ServerSettings.getInstance().getWeb().getAllowedOrigins();
291        if (origins == null) {
292            return;
293        }
294
295        logger.debug("Applying CORS filter to {}", handler);
296        if (handler instanceof ServletContextHandler) {
297            ServletContextHandler svHandler = (ServletContextHandler)handler;
298            this.addCORSFilter(svHandler);
299            logger.debug("Applied CORS filter to {}!", svHandler);
300        } else if (handler instanceof HandlerWrapper) {
301            for (Handler h : ((HandlerWrapper)handler).getHandlers()) {
302                addCORSFilter(h);
303            }
304        } else if (handler instanceof HandlerCollection) {
305            for (Handler h : ((HandlerCollection)handler).getHandlers()) {
306                addCORSFilter(h);
307            }
308        }
309    }
310
311    /**
312     * Stops the Dicoogle Web service.
313     * @throws java.lang.Exception if a problem occurs when stopping the server
314     */
315    public void stop() throws Exception {
316        // abort if the server is not running
317        if (server == null) {
318            return;
319        }
320
321        try {
322            // stop the server
323            server.stop();
324
325            // voiding its value
326            server = null;
327        } finally {
328
329            // and remove the local cache, if any
330            if (cache != null) {
331                cache.terminate();
332                cache = null;
333            }
334        }
335        
336        this.pluginHandler = null;
337    }
338
339    public void addContextHandlers(HandlerList handler) {
340        this.contextHandlers.addHandler(handler);
341
342        this.addCORSFilter(handler);
343        //this.server.setHandler(this.contextHandlers);
344    }
345
346    public void stopPluginWebServices() {
347        if (this.pluginHandler != null) {
348            this.contextHandlers.removeHandler(this.pluginHandler);
349        }
350    }
351 
352    public void stopLegacyWebServices() {
353        if (this.legacyHandler != null) {
354            this.contextHandlers.removeHandler(this.legacyHandler);
355        }
356    }
357
358}