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}