/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.oaipmh.server; import static java.lang.String.format; import static org.opencastproject.oaipmh.util.OsgiUtil.checkDictionary; import static org.opencastproject.oaipmh.util.OsgiUtil.getCfg; import static org.opencastproject.oaipmh.util.OsgiUtil.getContextProperty; import static org.opencastproject.util.data.Collections.map; import static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.some; import static org.opencastproject.util.data.functions.Strings.trimToNil; import org.opencastproject.oaipmh.util.XmlGen; import org.opencastproject.security.api.SecurityService; import org.opencastproject.util.OsgiUtil; import org.opencastproject.util.UrlSupport; import org.opencastproject.util.data.Option; import org.apache.commons.lang3.StringUtils; import org.osgi.framework.ServiceRegistration; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Dictionary; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** The OAI-PMH server. Backed by an arbitrary amount of OAI-PMH repositories. */ public final class OaiPmhServer extends HttpServlet implements OaiPmhServerInfo, ManagedService { private static final long serialVersionUID = -7536526468920288612L; private static final Logger logger = LoggerFactory.getLogger(OaiPmhServer.class); private static final String CFG_DEFAULT_REPOSITORY = "default-repository"; private static final String CFG_OAIPMH_MOUNTPOINT = "org.opencastproject.oaipmh.mountpoint"; private SecurityService securityService; private final Map<String, OaiPmhRepository> repositories = map(); private ComponentContext componentContext; private String defaultRepo; private boolean published = false; /** * The alias under which the servlet is currently registered. */ private String mountPoint; private ServiceRegistration<?> serviceRegistration; /** OSGi DI. */ public void setRepository(final OaiPmhRepository r) { synchronized (repositories) { final String rId = r.getRepositoryId(); if (repositories.containsKey(rId)) { logger.error(format("A repository with id %s has already been registered", rId)); } else { // lazy creation since 'baseUrl' is not available at this time repositories.put(rId, r); logger.info("Registered repository " + rId); } } } /** OSGi DI. */ public void unsetRepository(OaiPmhRepository r) { synchronized (repositories) { repositories.remove(r.getRepositoryId()); logger.info("Unregistered repository " + r.getRepositoryId()); } } /** OSGi DI. */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi component activation. */ public synchronized void activate(ComponentContext cc) { logger.info("Activate"); this.componentContext = cc; // get mount point mountPoint = UrlSupport.concat("/", getContextProperty(componentContext, CFG_OAIPMH_MOUNTPOINT)); } /** Called by the ConfigurationAdmin service. This method actually sets up the server. */ @Override public synchronized void updated(Dictionary<String, ?> properties) throws ConfigurationException { // Because the OAI-PMH server implementation is technically not a REST service implemented // using JAX-RS annotations the Matterhorn mechanisms for registering REST endpoints do not work. // The server has to register itself with the OSGi HTTP service. logger.info("Updated"); checkDictionary(properties, componentContext); defaultRepo = getCfg(properties, CFG_DEFAULT_REPOSITORY); // register servlet try { // ... and unregister first if necessary tryUnregisterServlet(); logger.info("Registering OAI-PMH server under " + mountPoint); logger.info("Default repository is " + defaultRepo); serviceRegistration = OsgiUtil.registerServlet(componentContext.getBundleContext(), this, mountPoint); } catch (Exception e) { logger.error("Error registering OAI-PMH servlet", e); throw new RuntimeException("Error registering OAI-PMH servlet", e); } logger.info(format("There are %d repositories registered yet. Watch out for later registration messages.", repositories.values().size())); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { dispatch(req, res); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { dispatch(req, res); } private void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws IOException { try { for (String serverUrl : OaiPmhServerInfoUtil.oaiPmhServerUrlOfCurrentOrganization(securityService)) { for (String repoId : repositoryId(req, mountPoint)) { if (runRepo(repoId, serverUrl, req, res)) { return; } else { res.sendError(HttpServletResponse.SC_NOT_FOUND); return; } } // no repository id in path, try default repo if (runRepo(defaultRepo, serverUrl, req, res)) { return; } } res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); } catch (Exception e) { logger.error("Error handling OAI-PMH request", e); res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); } } /** * Run repo <code>repoId</code>. * * @return false if the repo does not exist, true otherwise */ private boolean runRepo(String repoId, String serverUrl, HttpServletRequest req, HttpServletResponse res) throws Exception { for (OaiPmhRepository repo : getRepoById(repoId)) { final String repoUrl = UrlSupport.concat(serverUrl, mountPoint, repoId); runRepo(repo, repoUrl, req, res); return true; } return false; } private void runRepo(OaiPmhRepository repo, final String repoUrl, final HttpServletRequest req, HttpServletResponse res) throws Exception { final Params p = new Params() { @Override String getParameter(String key) { return req.getParameter(key); } @Override String getRepositoryUrl() { return repoUrl; } }; final XmlGen oai = repo.selectVerb(p); res.setCharacterEncoding("UTF-8"); res.setContentType("text/xml;charset=UTF-8"); oai.generate(res.getOutputStream()); } @Override public void destroy() { super.destroy(); tryUnregisterServlet(); } private void tryUnregisterServlet() { if (serviceRegistration != null) { serviceRegistration.unregister(); } } /** * Retrieve the repository id from the requested path. * * @param req * the HTTP request * @param mountPoint * the base path of the OAI-PMH server, e.g. /oaipmh */ public static Option<String> repositoryId(HttpServletRequest req, String mountPoint) { return mlist(StringUtils.removeStart(UrlSupport.removeDoubleSeparator(req.getRequestURI()), mountPoint).split("/")) .bind(trimToNil).headOpt(); } /** Get a repository by id. */ private Option<OaiPmhRepository> getRepoById(String id) { synchronized (repositories) { if (hasRepo(id)) { return some(repositories.get(id)); } else { logger.warn("No OAI-PMH repository has been registered with id " + id); return none(); } } } @Override public boolean hasRepo(String id) { synchronized (repositories) { return repositories.containsKey(id); } } @Override public String getMountPoint() { return mountPoint; } }