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("/",
133           StringUtils.trimToNull(getContextProperty(componentContext, CFG_OAIPMH_MOUNTPOINT)));
134     } catch (RuntimeException e) {
135       mountPoint = CFG_DEFAULT_OAIPMH_MOUNTPOINT;
136     }
137     updated(cc.getProperties());
138   }
139 
140   @Modified
141   public void modified(ComponentContext cc) throws ConfigurationException {
142     logger.info("Updated");
143     updated(cc.getProperties());
144   }
145 
146   @Deactivate
147   public void deactivate() {
148     tryUnregisterServlet();
149   }
150 
151   /** Called by the ConfigurationAdmin service. This method actually sets up the server. */
152   public synchronized void updated(Dictionary<String, ?> properties) throws ConfigurationException {
153     // Because the OAI-PMH server implementation is technically not a REST service implemented
154     // using JAX-RS annotations the Opencast mechanisms for registering REST endpoints do not work.
155     // The server has to register itself with the OSGi HTTP service.
156     checkDictionary(properties, componentContext);
157     defaultRepo = getCfg(properties, CFG_DEFAULT_REPOSITORY);
158     // register servlet
159     try {
160       // ... and unregister first if necessary
161       tryUnregisterServlet();
162       logger.info("Registering OAI-PMH server under " + mountPoint);
163       logger.info("Default repository is " + defaultRepo);
164 
165       serviceRegistration = OsgiUtil.registerServlet(componentContext.getBundleContext(), this, mountPoint);
166     } catch (Exception e) {
167       logger.error("Error registering OAI-PMH servlet", e);
168       throw new RuntimeException("Error registering OAI-PMH servlet", e);
169     }
170     logger.info("There are {} repositories registered yet. Watch out for later registration messages.",
171             repositories.values().size());
172   }
173 
174   @Override
175   protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
176     dispatch(req, res);
177   }
178 
179   @Override
180   protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
181     dispatch(req, res);
182   }
183 
184   private void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
185     try {
186       Optional<String> serverUrl = OaiPmhServerInfoUtil.oaiPmhServerUrlOfCurrentOrganization(securityService);
187       if (serverUrl.isPresent()) {
188         Optional<String> repositoryId = repositoryId(req, mountPoint);
189         if (repositoryId.isPresent()) {
190           if (runRepo(repositoryId.get(), serverUrl.get(), req, res)) {
191             return;
192           } else {
193             res.sendError(HttpServletResponse.SC_NOT_FOUND);
194             return;
195           }
196         }
197         // no repository id in path, try default repo
198         if (runRepo(defaultRepo, serverUrl.get(), req, res)) {
199           return;
200         }
201       }
202       res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
203     } catch (Exception e) {
204       logger.error("Error handling OAI-PMH request", e);
205       res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
206     }
207   }
208 
209   /**
210    * Run repo <code>repoId</code>.
211    *
212    * @return false if the repo does not exist, true otherwise
213    */
214   private boolean runRepo(String repoId, String serverUrl, HttpServletRequest req, HttpServletResponse res)
215           throws Exception {
216     Optional<OaiPmhRepository> repo = getRepoById(repoId);
217     if (repo.isPresent()) {
218       final String repoUrl = UrlSupport.concat(serverUrl, mountPoint, repoId);
219       runRepo(repo.get(), repoUrl, req, res);
220       return true;
221     }
222     return false;
223   }
224 
225   private void runRepo(OaiPmhRepository repo, final String repoUrl, final HttpServletRequest req,
226           HttpServletResponse res) throws Exception {
227     final Params p = new Params() {
228       @Override
229       String getParameter(String key) {
230         return req.getParameter(key);
231       }
232 
233       @Override
234       String getRepositoryUrl() {
235         return repoUrl;
236       }
237     };
238     final XmlGen oai = repo.selectVerb(p);
239     res.setCharacterEncoding("UTF-8");
240     res.setContentType("text/xml;charset=UTF-8");
241     oai.generate(res.getOutputStream());
242   }
243 
244   private synchronized void tryUnregisterServlet() {
245     if (serviceRegistration != null) {
246       serviceRegistration.unregister();
247       serviceRegistration = null;
248     }
249   }
250 
251   /**
252    * Retrieve the repository id from the requested path.
253    *
254    * @param req
255    *          the HTTP request
256    * @param mountPoint
257    *          the base path of the OAI-PMH server, e.g. /oaipmh
258    */
259   public static Optional<String> repositoryId(HttpServletRequest req, String mountPoint) {
260     String[] parts = StringUtils.removeStart(UrlSupport.removeDoubleSeparator(req.getRequestURI()), mountPoint)
261         .split("/");
262 
263     return Arrays.stream(parts)
264         .flatMap(s -> trimToNil(s).stream())
265         .findFirst();
266   }
267 
268   /** Get a repository by id. */
269   private Optional<OaiPmhRepository> getRepoById(String id) {
270     synchronized (repositories) {
271       if (hasRepo(id)) {
272         return Optional.of(repositories.get(id));
273       } else {
274         logger.warn("No OAI-PMH repository has been registered with id " + id);
275         return Optional.empty();
276       }
277     }
278   }
279 
280   @Override
281   public boolean hasRepo(String id) {
282     synchronized (repositories) {
283       return repositories.containsKey(id);
284     }
285   }
286 
287   @Override
288   public String getMountPoint() {
289     return mountPoint;
290   }
291 }