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.Monadics.mlist;
28  import static org.opencastproject.util.data.Option.none;
29  import static org.opencastproject.util.data.Option.some;
30  import static org.opencastproject.util.data.functions.Strings.trimToNil;
31  
32  import org.opencastproject.oaipmh.util.XmlGen;
33  import org.opencastproject.security.api.SecurityService;
34  import org.opencastproject.util.OsgiUtil;
35  import org.opencastproject.util.UrlSupport;
36  import org.opencastproject.util.data.Option;
37  
38  import org.apache.commons.lang3.StringUtils;
39  import org.osgi.framework.ServiceRegistration;
40  import org.osgi.service.cm.ConfigurationException;
41  import org.osgi.service.component.ComponentContext;
42  import org.osgi.service.component.annotations.Activate;
43  import org.osgi.service.component.annotations.Component;
44  import org.osgi.service.component.annotations.Deactivate;
45  import org.osgi.service.component.annotations.Modified;
46  import org.osgi.service.component.annotations.Reference;
47  import org.osgi.service.component.annotations.ReferenceCardinality;
48  import org.osgi.service.component.annotations.ReferencePolicy;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import java.io.IOException;
53  import java.util.Dictionary;
54  import java.util.Map;
55  
56  import javax.servlet.ServletException;
57  import javax.servlet.http.HttpServlet;
58  import javax.servlet.http.HttpServletRequest;
59  import javax.servlet.http.HttpServletResponse;
60  
61  /** The OAI-PMH server. Backed by an arbitrary amount of OAI-PMH repositories. */
62  @Component(
63      immediate = true,
64      service = { OaiPmhServerInfo.class, OaiPmhServer.class },
65      property = {
66          "service.description=OAI-PMH server"
67      }
68  )
69  public final class OaiPmhServer extends HttpServlet implements OaiPmhServerInfo {
70  
71    private static final long serialVersionUID = -7536526468920288612L;
72  
73    private static final Logger logger = LoggerFactory.getLogger(OaiPmhServer.class);
74  
75    private static final String CFG_DEFAULT_REPOSITORY = "default-repository";
76    private static final String CFG_OAIPMH_MOUNTPOINT = "org.opencastproject.oaipmh.mountpoint";
77    private static final String CFG_DEFAULT_OAIPMH_MOUNTPOINT = "/oaipmh";
78  
79    private SecurityService securityService;
80  
81    private final Map<String, OaiPmhRepository> repositories = map();
82  
83    private ComponentContext componentContext;
84  
85    private String defaultRepo;
86  
87    /**
88     * The alias under which the servlet is currently registered.
89     */
90    private String mountPoint;
91  
92    private ServiceRegistration<?> serviceRegistration;
93  
94    /** OSGi DI. */
95    @Reference(
96        cardinality = ReferenceCardinality.MULTIPLE,
97        policy = ReferencePolicy.DYNAMIC,
98        unbind = "unsetRepository"
99    )
100   public void setRepository(final OaiPmhRepository r) {
101     synchronized (repositories) {
102       final String rId = r.getRepositoryId();
103       if (repositories.containsKey(rId)) {
104         logger.error("A repository with id {} has already been registered", rId);
105       } else {
106         // lazy creation since 'baseUrl' is not available at this time
107         repositories.put(rId, r);
108         logger.info("Registered repository " + rId);
109       }
110     }
111   }
112 
113   /** OSGi DI. */
114   public void unsetRepository(OaiPmhRepository r) {
115     synchronized (repositories) {
116       repositories.remove(r.getRepositoryId());
117       logger.info("Unregistered repository " + r.getRepositoryId());
118     }
119   }
120 
121   /** OSGi DI. */
122   @Reference
123   public void setSecurityService(SecurityService securityService) {
124     this.securityService = securityService;
125   }
126 
127   /** OSGi component activation. */
128   @Activate
129   public void activate(ComponentContext cc) throws ConfigurationException {
130     logger.info("Activate");
131     this.componentContext = cc;
132     // get mount point
133     try {
134         mountPoint = UrlSupport.concat("/", StringUtils.trimToNull(getContextProperty(componentContext, CFG_OAIPMH_MOUNTPOINT)));
135     } catch (RuntimeException e) {
136         mountPoint = CFG_DEFAULT_OAIPMH_MOUNTPOINT;
137     }
138     updated(cc.getProperties());
139   }
140 
141   @Modified
142   public void modified(ComponentContext cc) throws ConfigurationException {
143     logger.info("Updated");
144     updated(cc.getProperties());
145   }
146 
147   @Deactivate
148   public void deactivate() {
149     tryUnregisterServlet();
150   }
151 
152   /** Called by the ConfigurationAdmin service. This method actually sets up the server. */
153   public synchronized void updated(Dictionary<String, ?> properties) throws ConfigurationException {
154     // Because the OAI-PMH server implementation is technically not a REST service implemented
155     // using JAX-RS annotations the Opencast mechanisms for registering REST endpoints do not work.
156     // The server has to register itself with the OSGi HTTP service.
157     checkDictionary(properties, componentContext);
158     defaultRepo = getCfg(properties, CFG_DEFAULT_REPOSITORY);
159     // register servlet
160     try {
161       // ... and unregister first if necessary
162       tryUnregisterServlet();
163       logger.info("Registering OAI-PMH server under " + mountPoint);
164       logger.info("Default repository is " + defaultRepo);
165 
166       serviceRegistration = OsgiUtil.registerServlet(componentContext.getBundleContext(), this, mountPoint);
167     } catch (Exception e) {
168       logger.error("Error registering OAI-PMH servlet", e);
169       throw new RuntimeException("Error registering OAI-PMH servlet", e);
170     }
171     logger.info("There are {} repositories registered yet. Watch out for later registration messages.",
172             repositories.values().size());
173   }
174 
175   @Override
176   protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
177     dispatch(req, res);
178   }
179 
180   @Override
181   protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
182     dispatch(req, res);
183   }
184 
185   private void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
186     try {
187       for (String serverUrl : OaiPmhServerInfoUtil.oaiPmhServerUrlOfCurrentOrganization(securityService)) {
188         for (String repoId : repositoryId(req, mountPoint)) {
189           if (runRepo(repoId, serverUrl, 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, 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     for (OaiPmhRepository repo : getRepoById(repoId)) {
216       final String repoUrl = UrlSupport.concat(serverUrl, mountPoint, repoId);
217       runRepo(repo, repoUrl, req, res);
218       return true;
219     }
220     return false;
221   }
222 
223   private void runRepo(OaiPmhRepository repo, final String repoUrl, final HttpServletRequest req,
224           HttpServletResponse res) throws Exception {
225     final Params p = new Params() {
226       @Override
227       String getParameter(String key) {
228         return req.getParameter(key);
229       }
230 
231       @Override
232       String getRepositoryUrl() {
233         return repoUrl;
234       }
235     };
236     final XmlGen oai = repo.selectVerb(p);
237     res.setCharacterEncoding("UTF-8");
238     res.setContentType("text/xml;charset=UTF-8");
239     oai.generate(res.getOutputStream());
240   }
241 
242   private synchronized void tryUnregisterServlet() {
243     if (serviceRegistration != null) {
244       serviceRegistration.unregister();
245       serviceRegistration = null;
246     }
247   }
248 
249   /**
250    * Retrieve the repository id from the requested path.
251    *
252    * @param req
253    *          the HTTP request
254    * @param mountPoint
255    *          the base path of the OAI-PMH server, e.g. /oaipmh
256    */
257   public static Option<String> repositoryId(HttpServletRequest req, String mountPoint) {
258     return mlist(StringUtils.removeStart(UrlSupport.removeDoubleSeparator(req.getRequestURI()), mountPoint).split("/"))
259             .bind(trimToNil).headOpt();
260   }
261 
262   /** Get a repository by id. */
263   private Option<OaiPmhRepository> getRepoById(String id) {
264     synchronized (repositories) {
265       if (hasRepo(id)) {
266         return some(repositories.get(id));
267       } else {
268         logger.warn("No OAI-PMH repository has been registered with id " + id);
269         return none();
270       }
271     }
272   }
273 
274   @Override
275   public boolean hasRepo(String id) {
276     synchronized (repositories) {
277       return repositories.containsKey(id);
278     }
279   }
280 
281   @Override
282   public String getMountPoint() {
283     return mountPoint;
284   }
285 }