EntryStoreApplication.java

  1. /*
  2.  * Copyright (c) 2007-2017 MetaSolutions AB
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *     http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */

  16. package org.entrystore.rest;

  17. import lombok.Getter;
  18. import org.apache.commons.fileupload.servlet.FileCleanerCleanup;
  19. import org.apache.commons.io.FileCleaningTracker;
  20. import org.entrystore.ContextManager;
  21. import org.entrystore.Converter;
  22. import org.entrystore.Entry;
  23. import org.entrystore.GraphType;
  24. import org.entrystore.PrincipalManager;
  25. import org.entrystore.config.Config;
  26. import org.entrystore.harvester.Harvester;
  27. import org.entrystore.harvester.factory.HarvesterFactoryException;
  28. import org.entrystore.harvesting.oaipmh.harvester.factory.OAIHarvesterFactory;
  29. import org.entrystore.impl.RepositoryManagerImpl;
  30. import org.entrystore.impl.converters.ConverterManagerImpl;
  31. import org.entrystore.impl.converters.OAI_DC2RDFGraphConverter;
  32. import org.entrystore.repository.backup.BackupScheduler;
  33. import org.entrystore.repository.config.ConfigurationManager;
  34. import org.entrystore.repository.config.Settings;
  35. import org.entrystore.repository.security.Password;
  36. import org.entrystore.repository.test.TestSuite;
  37. import org.entrystore.rest.auth.BasicVerifier;
  38. import org.entrystore.rest.auth.CookieVerifier;
  39. import org.entrystore.rest.auth.LoginTokenCache;
  40. import org.entrystore.rest.auth.SimpleAuthenticator;
  41. import org.entrystore.rest.auth.UserTempLockoutCache;
  42. import org.entrystore.rest.filter.CORSFilter;
  43. import org.entrystore.rest.filter.CacheControlFilter;
  44. import org.entrystore.rest.filter.IgnoreAuthFilter;
  45. import org.entrystore.rest.filter.JSCallbackFilter;
  46. import org.entrystore.rest.filter.ModificationLockOutFilter;
  47. import org.entrystore.rest.filter.PerformanceMetricsFilter;
  48. import org.entrystore.rest.resources.CasLoginResource;
  49. import org.entrystore.rest.resources.ContextResource;
  50. import org.entrystore.rest.resources.CookieLoginResource;
  51. import org.entrystore.rest.resources.DefaultResource;
  52. import org.entrystore.rest.resources.EchoResource;
  53. import org.entrystore.rest.resources.EntryResource;
  54. import org.entrystore.rest.resources.ExecutionResource;
  55. import org.entrystore.rest.resources.ExportResource;
  56. import org.entrystore.rest.resources.ExternalMetadataResource;
  57. import org.entrystore.rest.resources.FaviconResource;
  58. import org.entrystore.rest.resources.GroupResource;
  59. import org.entrystore.rest.resources.HarvesterResource;
  60. import org.entrystore.rest.resources.ImportResource;
  61. import org.entrystore.rest.resources.IndexResource;
  62. import org.entrystore.rest.resources.LegacySamlLoginResource;
  63. import org.entrystore.rest.resources.LocalMetadataResource;
  64. import org.entrystore.rest.resources.LoggingResource;
  65. import org.entrystore.rest.resources.LoginResource;
  66. import org.entrystore.rest.resources.LogoutResource;
  67. import org.entrystore.rest.resources.LookupResource;
  68. import org.entrystore.rest.resources.MergeResource;
  69. import org.entrystore.rest.resources.MergedMetadataResource;
  70. import org.entrystore.rest.resources.MessageResource;
  71. import org.entrystore.rest.resources.NameResource;
  72. import org.entrystore.rest.resources.PasswordResetResource;
  73. import org.entrystore.rest.resources.PerformanceMetricsResource;
  74. import org.entrystore.rest.resources.ProxyResource;
  75. import org.entrystore.rest.resources.QuotaResource;
  76. import org.entrystore.rest.resources.RelationResource;
  77. import org.entrystore.rest.resources.ResourceResource;
  78. import org.entrystore.rest.resources.SamlLoginResource;
  79. import org.entrystore.rest.resources.SearchResource;
  80. import org.entrystore.rest.resources.ShutdownResource;
  81. import org.entrystore.rest.resources.SignupResource;
  82. import org.entrystore.rest.resources.SolrResource;
  83. import org.entrystore.rest.resources.SparqlResource;
  84. import org.entrystore.rest.resources.StatisticsResource;
  85. import org.entrystore.rest.resources.StatusResource;
  86. import org.entrystore.rest.resources.TokenResource;
  87. import org.entrystore.rest.resources.UserResource;
  88. import org.entrystore.rest.resources.ValidatorResource;
  89. import org.entrystore.rest.util.CORSUtil;
  90. import org.restlet.Application;
  91. import org.restlet.Component;
  92. import org.restlet.Context;
  93. import org.restlet.Request;
  94. import org.restlet.Response;
  95. import org.restlet.Restlet;
  96. import org.restlet.data.ChallengeScheme;
  97. import org.restlet.data.Reference;
  98. import org.restlet.routing.Filter;
  99. import org.restlet.routing.Router;
  100. import org.restlet.security.ChallengeAuthenticator;
  101. import org.slf4j.Logger;
  102. import org.slf4j.LoggerFactory;

  103. import javax.naming.InitialContext;
  104. import javax.naming.NamingException;
  105. import javax.servlet.ServletContext;
  106. import java.io.File;
  107. import java.io.IOException;
  108. import java.net.URI;
  109. import java.util.ArrayList;
  110. import java.util.Date;
  111. import java.util.HashSet;
  112. import java.util.Map;
  113. import java.util.Set;

  114. /**
  115.  * Main class to start EntryStore as Restlet Application.
  116.  *
  117.  * @author Hannes Ebner
  118.  */
  119. public class EntryStoreApplication extends Application {

  120.     /** Logger */
  121.     static Logger log = LoggerFactory.getLogger(EntryStoreApplication.class);

  122.     public static final String KEY = EntryStoreApplication.class.getCanonicalName();
  123.     public static final String ENV_CONFIG_URI = "ENTRYSTORE_CONFIG_URI";
  124.     public static final String OAI_DC = "oai_dc";
  125.     public static final String RDN_DC = "rdn_dc";
  126.     @Getter
  127.     private static Date startupDate;


  128.     /** Central point for accessing a repository */
  129.     private RepositoryManagerImpl rm;

  130.     private BackupScheduler backupScheduler;
  131.     @Getter
  132.     private ArrayList<Harvester> harvesters = new ArrayList<>();
  133.     private final Component component;

  134.     private final ContextManager cm;
  135.     private final PrincipalManager pm;

  136.     @Getter
  137.     private LoginTokenCache loginTokenCache = null;
  138.     @Getter
  139.     private final UserTempLockoutCache userTempLockoutCache;
  140.     @Getter
  141.     private final Set<String> reservedNames = new HashSet<>();

  142.     public EntryStoreApplication(Context parentContext) {
  143.         this(null, parentContext, null);
  144.     }

  145.     public EntryStoreApplication(URI configPath, Context parentContext, Component component) {
  146.         this(configPath, null, parentContext, component);
  147.     }

  148.     public EntryStoreApplication(URI configPath, Map<String, String> configOverride, Context parentContext, Component component) {
  149.         super(parentContext);
  150.         Date startupBegin = new Date();
  151.         this.component = component;
  152.         URI configURI = configPath;
  153.         getContext().getAttributes().put(KEY, this);

  154.         /*
  155.          * should fix the hangs in Acrobat Reader that sometimes occur when
  156.          * Acrobat tries to fetch parts of files
  157.          */
  158.         getRangeService().setEnabled(false);
  159.         log.warn("Restlet RangeService deactivated");

  160.         if (getServletContext(getContext()) != null) {
  161.             // Application created by ServerServlet, try to get RepositoryManager from ServletContext
  162.             rm = (RepositoryManagerImpl) getServletContext(getContext()).getAttribute("RepositoryManager");
  163.         }

  164.         if (rm != null) {
  165.             // Initialized by a ServletContextListener
  166.             log.info("EntryStore initialized with a ServletContextListener");
  167.             cm = rm.getContextManager();
  168.             pm = rm.getPrincipalManager();

  169.             Config config = rm.getConfiguration();
  170.             this.loginTokenCache = new LoginTokenCache(config);

  171.             // The following objects are fetched from the context attributes,
  172.             // after they have been set in the ContextLoaderListener
  173.             harvesters = (ArrayList) getContext().getAttributes().get("Harvesters");
  174.             backupScheduler = (BackupScheduler) getContext().getAttributes().get("BackupScheduler");
  175.             userTempLockoutCache = null;
  176.         } else {
  177.             if (configURI == null) {
  178.                 // First we check for a config URI in an environment variable
  179.                 String envConfigURI = System.getenv(ENV_CONFIG_URI);
  180.                 if (envConfigURI != null) {
  181.                     configURI = URI.create(envConfigURI);
  182.                 } else {
  183.                     // We try the context
  184.                     javax.naming.Context env;
  185.                     try {
  186.                         env = (javax.naming.Context) new InitialContext().lookup("java:comp/env");
  187.                         if (env != null && env.lookup("entrystore.config") != null) {
  188.                             configURI = new File((String) env.lookup("entrystore.config")).toURI();
  189.                         }
  190.                     } catch (NamingException e) {
  191.                         log.warn(e.getMessage());
  192.                     }
  193.                 }
  194.             }

  195.             // Initialize EntryStore
  196.             ConfigurationManager confManager;
  197.             try {
  198.                 if (configURI != null) {
  199.                     log.info("Manually specified config location at {}", configURI);
  200.                     confManager = new ConfigurationManager(configURI);
  201.                 } else {
  202.                     log.info("No config location specified, looking within classpath");
  203.                     confManager = new ConfigurationManager(ConfigurationManager.getConfigurationURI());
  204.                 }
  205.             } catch (IOException e) {
  206.                 confManager = null;
  207.                 log.error("Unable to load configuration: {}", e.getMessage());
  208.                 System.exit(1);
  209.             }

  210.             Config config = confManager.getConfiguration();
  211.             if (configOverride != null) {
  212.                 config.getProperties().putAll(configOverride);
  213.             }

  214.             // see https://github.com/eclipse-rdf4j/rdf4j/issues/5148
  215.             System.setProperty(Settings.RDF4J_SOFT_FAIL_ON_CORRUPT_DATA_AND_REPAIR_INDEXES, config.getString(Settings.RDF4J_SOFT_FAIL_ON_CORRUPT_DATA_AND_REPAIR_INDEXES, "false"));

  216.             String baseURI = config.getString(Settings.BASE_URL);
  217.             if (baseURI == null) {
  218.                 log.error("No Base URI specified, exiting");
  219.                 System.exit(1);
  220.             }

  221.             Converter oaiDcRdfConverter = new OAI_DC2RDFGraphConverter();
  222.             ConverterManagerImpl.register(OAI_DC, oaiDcRdfConverter);
  223.             ConverterManagerImpl.register(RDN_DC, oaiDcRdfConverter);

  224.             this.rm = new RepositoryManagerImpl(baseURI, confManager.getConfiguration());
  225.             this.cm = rm.getContextManager();
  226.             this.pm = rm.getPrincipalManager();
  227.             this.userTempLockoutCache = new UserTempLockoutCache(rm, pm);
  228.             this.loginTokenCache = new LoginTokenCache(confManager.getConfiguration());
  229.             Password.loadRules(config);

  230.             if ("on".equalsIgnoreCase(config.getString(Settings.STORE_INIT_WITH_TEST_DATA, "off"))) {
  231.                 // Check for the existence of Donald
  232.                 Entry donald = rm.getPrincipalManager().getPrincipalEntry("Donald");
  233.                 // We only initialize of test suite has not been loaded before,
  234.                 // otherwise we end up with duplicates (if store is persisted)
  235.                 if (donald == null) {
  236.                     log.info("Initializing store with test data");
  237.                     // Create contexts, entries, etc for testing purposes
  238.                     TestSuite.initDisneySuite(rm);
  239.                     TestSuite.addEntriesInDisneySuite(rm);
  240.                     // TestSuite.initCourseSuite(rm);
  241.                 } else {
  242.                     log.warn("Test data is already present, not loading it again");
  243.                 }
  244.             }

  245.             // Load and start harvesters
  246.             startHarvesters();

  247.             // Load and start backup scheduler
  248.             boolean backup = "on".equalsIgnoreCase(rm.getConfiguration().getString(Settings.BACKUP_SCHEDULER, "off"));
  249.             if (backup) {
  250.                 log.info("Starting backup scheduler");
  251.                 startBackupScheduler();
  252.             } else {
  253.                 log.warn("Backup is disabled in configuration");
  254.             }

  255.         }
  256.         startupDate = new Date();
  257.         log.info("EntryStore startup completed in {} ms", startupDate.getTime() - startupBegin.getTime());
  258.     }

  259.     /**
  260.      * Creates a root Restlet that will receive all incoming calls.
  261.      * <p>
  262.      * Because Restlets impose no restrictions on resource design,
  263.      * the resource classes and the URIs, they expose flow naturally
  264.      * from considerations of ROA design. Below, you have a mapping from
  265.      * URIs to the resources in the REST module.
  266.      */
  267.     @Override
  268.     public synchronized Restlet createInboundRoot() {
  269.         Config config = rm.getConfiguration();
  270.         Router router = new Router(getContext());
  271.         //router.setDefaultMatchingMode(Template.MODE_STARTS_WITH);

  272.         boolean passwordAuthOff = "off".equalsIgnoreCase(config.getString(Settings.AUTH_PASSWORD, "on"));

  273.         // to prevent unnecessary context-id lookups we route favicon.ico to a real icon
  274.         reservedNames.add("favicon.ico");
  275.         router.attach("/favicon.ico", FaviconResource.class);

  276.         // global scope
  277.         reservedNames.add("echo");
  278.         router.attach("/echo", EchoResource.class);

  279.         reservedNames.add("lookup");
  280.         router.attach("/lookup", LookupResource.class);

  281.         reservedNames.add("proxy");
  282.         router.attach("/proxy", ProxyResource.class);

  283.         reservedNames.add("search");
  284.         router.attach("/search", SearchResource.class);

  285.         reservedNames.add("sparql");
  286.         router.attach("/sparql", SparqlResource.class);

  287.         reservedNames.add("validator");
  288.         router.attach("/validator", ValidatorResource.class);

  289.         reservedNames.add("message");
  290.         router.attach("/message", MessageResource.class);

  291.         // authentication resources
  292.         reservedNames.add("auth");
  293.         if (!passwordAuthOff) {
  294.             // we allow login with username/password
  295.             router.attach("/auth/cookie", CookieLoginResource.class);
  296.             router.attach("/auth/login", LoginResource.class);
  297.         }
  298.         router.attach("/auth/user", UserResource.class);
  299.         router.attach("/auth/basic", UserResource.class);
  300.         router.attach("/auth/logout", LogoutResource.class);
  301.         router.attach("/auth/tokens", TokenResource.class);

  302.         // CAS
  303.         if ("on".equalsIgnoreCase(config.getString(Settings.AUTH_CAS, "off"))) {
  304.             router.attach("/auth/cas", CasLoginResource.class);
  305.             log.info("CAS authentication enabled");
  306.         }

  307.         // SAML
  308.         if ("on".equalsIgnoreCase(config.getString(Settings.AUTH_SAML))) {
  309.             router.attach("/auth/saml", LegacySamlLoginResource.class);
  310.             log.info("Legacy SAML authentication enabled");
  311.         } else if ("new".equalsIgnoreCase(config.getString(Settings.AUTH_SAML))) {
  312.             // FIXME make "new" default (activate for "on") and provide legacy only via "legacy" in a future version
  313.             router.attach("/auth/saml", SamlLoginResource.class);
  314.             log.info("SAML authentication enabled");
  315.         }

  316.         // signup
  317.         if ("on".equalsIgnoreCase(config.getString(Settings.SIGNUP, "off"))) {
  318.             router.attach("/auth/signup", SignupResource.class);
  319.         }

  320.         // password reset
  321.         if ("on".equalsIgnoreCase(config.getString(Settings.AUTH_PASSWORD_RESET, "off"))) {
  322.             router.attach("/auth/pwreset", PasswordResetResource.class);
  323.         }

  324.         // management/configuration resources
  325.         reservedNames.add("management");
  326.         router.attach("/management/logging", LoggingResource.class);
  327.         router.attach("/management/status", StatusResource.class);
  328.         router.attach("/management/solr", SolrResource.class);
  329.         router.attach("/management/metrics", PerformanceMetricsResource.class);
  330.         router.attach("/management/shutdown", ShutdownResource.class);

  331.         // context scope
  332.         router.attach("/{context-id}", ContextResource.class);
  333.         router.attach("/{context-id}/sparql", SparqlResource.class);
  334.         router.attach("/{context-id}/export", ExportResource.class);
  335.         router.attach("/{context-id}/import", ImportResource.class);
  336.         router.attach("/{context-id}/merge", MergeResource.class);
  337.         router.attach("/{context-id}/statistics/{stat-type}", StatisticsResource.class);
  338.         router.attach("/{context-id}/entry/{entry-id}", EntryResource.class);
  339.         router.attach("/{context-id}/entry/{entry-id}/index", IndexResource.class);
  340.         router.attach("/{context-id}/entry/{entry-id}/name", NameResource.class);
  341.         router.attach("/{context-id}/resource/{entry-id}", ResourceResource.class);
  342.         router.attach("/{context-id}/metadata/{entry-id}", LocalMetadataResource.class);
  343.         router.attach("/{context-id}/cached-external-metadata/{entry-id}", ExternalMetadataResource.class);
  344.         router.attach("/{context-id}/merged-metadata/{entry-id}", MergedMetadataResource.class);
  345.         router.attach("/{context-id}/harvester", HarvesterResource.class);
  346.         router.attach("/{context-id}/relations/{entry-id}", RelationResource.class);
  347.         router.attach("/{context-id}/quota", QuotaResource.class);
  348.         router.attach("/{context-id}/lookup", LookupResource.class);
  349.         router.attach("/{context-id}/execute", ExecutionResource.class);
  350.         router.attach("/{context-id}/proxy", ProxyResource.class);

  351.         // principals scope
  352.         router.attach("/_principals/groups", GroupResource.class);

  353.         // default resource
  354.         router.attachDefault(DefaultResource.class);

  355.         CORSFilter corsFilter = new CORSFilter(CORSUtil.getInstance(config));
  356.         boolean optional = !config.getBoolean(Settings.AUTH_COOKIE_INVALID_TOKEN_ERROR, true);
  357.         CookieVerifier cookieVerifier = new CookieVerifier(this, rm, corsFilter);
  358.         ChallengeAuthenticator cookieAuth = new SimpleAuthenticator(getContext(), optional, ChallengeScheme.HTTP_COOKIE, "EntryStore", cookieVerifier, pm);

  359.         IgnoreAuthFilter ignoreAuth = new IgnoreAuthFilter();
  360.         ModificationLockOutFilter modLockOut = new ModificationLockOutFilter();
  361.         JSCallbackFilter jsCallback = new JSCallbackFilter(config);
  362.         CacheControlFilter cacheControl = new CacheControlFilter();
  363.         PerformanceMetricsFilter performanceMetrics = new PerformanceMetricsFilter();

  364.         ignoreAuth.setNext(cookieAuth);

  365.         // If password authentication is disabled, we only allow cookie verification (as this may verify auth_tokens
  366.         // generated through a CAS-login), but not basic authentication (as this always requires username/password).
  367.         // Also, we only allow HTTP Basic authentication if explicitly enabled in configuration.
  368.         if (passwordAuthOff || !config.getBoolean(Settings.AUTH_HTTP_BASIC, false)) {
  369.             cookieAuth.setNext(jsCallback);
  370.         } else {
  371.             ChallengeAuthenticator basicAuth = new SimpleAuthenticator(getContext(), false, ChallengeScheme.HTTP_BASIC, "EntryStore", new BasicVerifier(pm, config), pm);
  372.             cookieAuth.setNext(basicAuth);
  373.             basicAuth.setNext(jsCallback);
  374.         }

  375.         jsCallback.setNext(performanceMetrics);
  376.         performanceMetrics.setNext(cacheControl);
  377.         cacheControl.setNext(modLockOut);

  378.         if ("on".equalsIgnoreCase(config.getString(Settings.CORS, "off"))) {
  379.             log.info("Enabling CORS");
  380.             modLockOut.setNext(corsFilter);
  381.             corsFilter.setNext(router);
  382.         } else {
  383.             modLockOut.setNext(router);
  384.         }

  385.         if (config.getBoolean(Settings.REPOSITORY_REWRITE_BASEREFERENCE, true)) {
  386.             // The following Filter resolves a problem that occurs with reverse
  387.             // proxying, i.e., the internal base reference (as seen e.g., by Tomcat)
  388.             // is different from the external one (as seen e.g., by Apache)
  389.             log.info("Rewriting of base reference is enabled");
  390.             Filter referenceFix = new Filter(getContext()) {
  391.                 @Override
  392.                 protected int beforeHandle(Request request, Response response) {
  393.                     Reference origRef = request.getResourceRef();
  394.                     String newBaseRef = rm.getRepositoryURL().toString();
  395.                     if (newBaseRef.endsWith("/")) {
  396.                         newBaseRef = newBaseRef.substring(0, newBaseRef.length() - 1);
  397.                     }
  398.                     origRef.setIdentifier(newBaseRef + origRef.getRemainingPart());
  399.                     origRef.setBaseRef(newBaseRef);
  400.                     return super.beforeHandle(request, response);
  401.                 }
  402.             };
  403.             referenceFix.setNext(ignoreAuth);
  404.             return referenceFix;
  405.         } else {
  406.             log.warn("Rewriting of base reference has been manually disabled");
  407.             return ignoreAuth;
  408.         }
  409.     }

  410.     public ContextManager getCM() {
  411.         return this.cm;
  412.     }

  413.     public PrincipalManager getPM() {
  414.         return this.pm;
  415.     }

  416.     public RepositoryManagerImpl getRM() {
  417.         return this.rm;
  418.     }

  419.     private void startBackupScheduler() {
  420.         URI userURI = getPM().getAuthenticatedUserURI();
  421.         try {
  422.             getPM().setAuthenticatedUserURI(getPM().getAdminUser().getURI());
  423.             BackupScheduler bs = BackupScheduler.getInstance(rm);
  424.             if (bs != null) {
  425.                 this.backupScheduler = bs;
  426.                 bs.run();
  427.             }
  428.         } finally {
  429.             getPM().setAuthenticatedUserURI(userURI);
  430.         }
  431.     }

  432.     private void startHarvesters() {
  433.         URI realURI = getPM().getAuthenticatedUserURI();
  434.         try {
  435.             getPM().setAuthenticatedUserURI(getPM().getAdminUser().getURI());
  436.             Set<URI> entries = getCM().getEntries();
  437.             for (URI entryURI : entries) {
  438.                 Entry entry = getCM().getByEntryURI(entryURI);

  439.                 if (entry == null) {
  440.                     log.warn("Entry with URI {} cannot be found and is null", entryURI);
  441.                     continue;
  442.                 }

  443.                 if (entry.getGraphType() == GraphType.Context) {
  444.                     OAIHarvesterFactory fac = new OAIHarvesterFactory();
  445.                     if (fac.isOAIHarvester(entry)) {
  446.                         try {
  447.                             Harvester har = fac.getHarvester(rm, entry.getEntryURI());
  448.                             har.run();
  449.                             harvesters.add(har);
  450.                         } catch (HarvesterFactoryException e) {
  451.                             log.error(e.getMessage());
  452.                         }
  453.                     }
  454.                 }
  455.             }
  456.         } finally {
  457.             getPM().setAuthenticatedUserURI(realURI);
  458.         }
  459.     }

  460.     public static String getVersion() {
  461.         return RepositoryManagerImpl.getVersion();
  462.     }

  463.     @Override
  464.     public synchronized void stop() throws Exception {
  465.         log.info("Shutting down");
  466.         if (rm != null) {
  467.             rm.shutdown();
  468.         }
  469.         if (getServletContext(getContext()) != null) {
  470.             FileCleaningTracker fct = FileCleanerCleanup.getFileCleaningTracker(getServletContext(getContext()));
  471.             if (fct != null) {
  472.                 log.info("Shutting down file cleaning tracker");
  473.                 fct.exitWhenFinished();
  474.             }
  475.         }
  476.         super.stop();
  477.     }

  478.     public static ServletContext getServletContext(Context context) {
  479.         ServletContext sc = null;
  480.         Context c = context.getServerDispatcher().getContext();
  481.         if (c != null) {
  482.             sc = (ServletContext) c.getAttributes().get("org.restlet.ext.servlet.ServletContext");
  483.         }
  484.         if (sc == null) {
  485.             sc = (ServletContext) context.getAttributes().get("org.restlet.ext.servlet.ServletContext");
  486.         }
  487.         return sc;
  488.     }

  489.     public void shutdownServer() {
  490.         if (this.component != null) {
  491.             log.info("Shutting down server");
  492.             try {
  493.                 this.component.stop();
  494.             } catch (Exception e) {
  495.                 throw new RuntimeException("Failed to stop server", e);
  496.             }
  497.         } else {
  498.                 log.info("Failed to shutdown server, component not set");
  499.         }
  500.     }
  501. }