View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  package org.opencastproject.oaipmh.server;
22  
23  import static org.opencastproject.oaipmh.util.OsgiUtil.checkDictionary;
24  import static org.opencastproject.oaipmh.util.OsgiUtil.getCfg;
25  import static org.opencastproject.oaipmh.util.OsgiUtil.getContextProperty;
26  import static org.opencastproject.util.data.Collections.map;
27  import static org.opencastproject.util.data.functions.Strings.trimToNil;
28  
29  import org.opencastproject.oaipmh.util.XmlGen;
30  import org.opencastproject.security.api.SecurityService;
31  import org.opencastproject.util.OsgiUtil;
32  import org.opencastproject.util.UrlSupport;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.osgi.framework.ServiceRegistration;
36  import org.osgi.service.cm.ConfigurationException;
37  import org.osgi.service.component.ComponentContext;
38  import org.osgi.service.component.annotations.Activate;
39  import org.osgi.service.component.annotations.Component;
40  import org.osgi.service.component.annotations.Deactivate;
41  import org.osgi.service.component.annotations.Modified;
42  import org.osgi.service.component.annotations.Reference;
43  import org.osgi.service.component.annotations.ReferenceCardinality;
44  import org.osgi.service.component.annotations.ReferencePolicy;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  import java.io.IOException;
49  import java.util.Arrays;
50  import java.util.Dictionary;
51  import java.util.Map;
52  import java.util.Optional;
53  
54  import javax.servlet.ServletException;
55  import javax.servlet.http.HttpServlet;
56  import javax.servlet.http.HttpServletRequest;
57  import javax.servlet.http.HttpServletResponse;
58  
59  /** The OAI-PMH server. Backed by an arbitrary amount of OAI-PMH repositories. */
60  @Component(
61      immediate = true,
62      service = { OaiPmhServerInfo.class, OaiPmhServer.class },
63      property = {
64          "service.description=OAI-PMH server"
65      }
66  )
67  public final class OaiPmhServer extends HttpServlet implements OaiPmhServerInfo {
68  
69    private static final long serialVersionUID = -7536526468920288612L;
70  
71    private static final Logger logger = LoggerFactory.getLogger(OaiPmhServer.class);
72  
73    private static final String CFG_DEFAULT_REPOSITORY = "default-repository";
74    private static final String CFG_OAIPMH_MOUNTPOINT = "org.opencastproject.oaipmh.mountpoint";
75    private static final String CFG_DEFAULT_OAIPMH_MOUNTPOINT = "/oaipmh";
76  
77    private SecurityService securityService;
78  
79    private final Map<String, OaiPmhRepository> repositories = map();
80  
81    private ComponentContext componentContext;
82  
83    private String defaultRepo;
84  
85    /**
86     * The alias under which the servlet is currently registered.
87     */
88    private String mountPoint;
89  
90    private ServiceRegistration<?> serviceRegistration;
91  
92    /** OSGi DI. */
93    @Reference(
94        cardinality = ReferenceCardinality.MULTIPLE,
95        policy = ReferencePolicy.DYNAMIC,
96        unbind = "unsetRepository"
97    )
98    public void setRepository(final OaiPmhRepository r) {
99      synchronized (repositories) {
100       final String rId = r.getRepositoryId();
101       if (repositories.containsKey(rId)) {
102         logger.error("A repository with id {} has already been registered", rId);
103       } else {
104         // lazy creation since 'baseUrl' is not available at this time
105         repositories.put(rId, r);
106         logger.info("Registered repository " + rId);
107       }
108     }
109   }
110 
111   /** OSGi DI. */
112   public void unsetRepository(OaiPmhRepository r) {
113     synchronized (repositories) {
114       repositories.remove(r.getRepositoryId());
115       logger.info("Unregistered repository " + r.getRepositoryId());
116     }
117   }
118 
119   /** OSGi DI. */
120   @Reference
121   public void setSecurityService(SecurityService securityService) {
122     this.securityService = securityService;
123   }
124 
125   /** OSGi component activation. */
126   @Activate
127   public void activate(ComponentContext cc) throws ConfigurationException {
128     logger.info("Activate");
129     this.componentContext = cc;
130     // get mount point
131     try {
132         mountPoint = UrlSupport.concat("/", StringUtils.trimToNull(getContextProperty(componentContext, CFG_OAIPMH_MOUNTPOINT)));
133     } catch (RuntimeException e) {
134         mountPoint = CFG_DEFAULT_OAIPMH_MOUNTPOINT;
135     }
136     updated(cc.getProperties());
137   }
138 
139   @Modified
140   public void modified(ComponentContext cc) throws ConfigurationException {
141     logger.info("Updated");
142     updated(cc.getProperties());
143   }
144 
145   @Deactivate
146   public void deactivate() {
147     tryUnregisterServlet();
148   }
149 
150   /** Called by the ConfigurationAdmin service. This method actually sets up the server. */
151   public synchronized void updated(Dictionary<String, ?> properties) throws ConfigurationException {
152     // Because the OAI-PMH server implementation is technically not a REST service implemented
153     // using JAX-RS annotations the Opencast mechanisms for registering REST endpoints do not work.
154     // The server has to register itself with the OSGi HTTP service.
155     checkDictionary(properties, componentContext);
156     defaultRepo = getCfg(properties, CFG_DEFAULT_REPOSITORY);
157     // register servlet
158     try {
159       // ... and unregister first if necessary
160       tryUnregisterServlet();
161       logger.info("Registering OAI-PMH server under " + mountPoint);
162       logger.info("Default repository is " + defaultRepo);
163 
164       serviceRegistration = OsgiUtil.registerServlet(componentContext.getBundleContext(), this, mountPoint);
165     } catch (Exception e) {
166       logger.error("Error registering OAI-PMH servlet", e);
167       throw new RuntimeException("Error registering OAI-PMH servlet", e);
168     }
169     logger.info("There are {} repositories registered yet. Watch out for later registration messages.",
170             repositories.values().size());
171   }
172 
173   @Override
174   protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
175     dispatch(req, res);
176   }
177 
178   @Override
179   protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
180     dispatch(req, res);
181   }
182 
183   private void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
184     try {
185       Optional<String> serverUrl = OaiPmhServerInfoUtil.oaiPmhServerUrlOfCurrentOrganization(securityService);
186       if (serverUrl.isPresent()) {
187         Optional<String> repositoryId = repositoryId(req, mountPoint);
188         if (repositoryId.isPresent()) {
189           if (runRepo(repositoryId.get(), serverUrl.get(), req, res)) {
190             return;
191           } else {
192             res.sendError(HttpServletResponse.SC_NOT_FOUND);
193             return;
194           }
195         }
196         // no repository id in path, try default repo
197         if (runRepo(defaultRepo, serverUrl.get(), req, res)) {
198           return;
199         }
200       }
201       res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
202     } catch (Exception e) {
203       logger.error("Error handling OAI-PMH request", e);
204       res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
205     }
206   }
207 
208   /**
209    * Run repo <code>repoId</code>.
210    *
211    * @return false if the repo does not exist, true otherwise
212    */
213   private boolean runRepo(String repoId, String serverUrl, HttpServletRequest req, HttpServletResponse res)
214           throws Exception {
215     Optional<OaiPmhRepository> repo = getRepoById(repoId);
216     if (repo.isPresent()) {
217       final String repoUrl = UrlSupport.concat(serverUrl, mountPoint, repoId);
218       runRepo(repo.get(), repoUrl, req, res);
219       return true;
220     }
221     return false;
222   }
223 
224   private void runRepo(OaiPmhRepository repo, final String repoUrl, final HttpServletRequest req,
225           HttpServletResponse res) throws Exception {
226     final Params p = new Params() {
227       @Override
228       String getParameter(String key) {
229         return req.getParameter(key);
230       }
231 
232       @Override
233       String getRepositoryUrl() {
234         return repoUrl;
235       }
236     };
237     final XmlGen oai = repo.selectVerb(p);
238     res.setCharacterEncoding("UTF-8");
239     res.setContentType("text/xml;charset=UTF-8");
240     oai.generate(res.getOutputStream());
241   }
242 
243   private synchronized void tryUnregisterServlet() {
244     if (serviceRegistration != null) {
245       serviceRegistration.unregister();
246       serviceRegistration = null;
247     }
248   }
249 
250   /**
251    * Retrieve the repository id from the requested path.
252    *
253    * @param req
254    *          the HTTP request
255    * @param mountPoint
256    *          the base path of the OAI-PMH server, e.g. /oaipmh
257    */
258   public static Optional<String> repositoryId(HttpServletRequest req, String mountPoint) {
259     String[] parts = StringUtils.removeStart(UrlSupport.removeDoubleSeparator(req.getRequestURI()), mountPoint).split("/");
260 
261     return Arrays.stream(parts)
262         .flatMap(s -> trimToNil(s).stream())
263         .findFirst();
264   }
265 
266   /** Get a repository by id. */
267   private Optional<OaiPmhRepository> getRepoById(String id) {
268     synchronized (repositories) {
269       if (hasRepo(id)) {
270         return Optional.of(repositories.get(id));
271       } else {
272         logger.warn("No OAI-PMH repository has been registered with id " + id);
273         return Optional.empty();
274       }
275     }
276   }
277 
278   @Override
279   public boolean hasRepo(String id) {
280     synchronized (repositories) {
281       return repositories.containsKey(id);
282     }
283   }
284 
285   @Override
286   public String getMountPoint() {
287     return mountPoint;
288   }
289 }