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.OaiPmhConstants.OAI_2_0_SCHEMA_LOCATION;
24  import static org.opencastproject.oaipmh.OaiPmhConstants.OAI_2_0_XML_NS;
25  import static org.opencastproject.oaipmh.OaiPmhUtil.toUtcSecond;
26  import static org.opencastproject.util.data.Option.none;
27  import static org.opencastproject.util.data.Option.some;
28  
29  import org.opencastproject.mediapackage.EName;
30  import org.opencastproject.metadata.dublincore.DublinCore;
31  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
32  import org.opencastproject.metadata.dublincore.DublinCoreValue;
33  import org.opencastproject.oaipmh.OaiPmhConstants;
34  import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseException;
35  import org.opencastproject.oaipmh.persistence.SearchResult;
36  import org.opencastproject.oaipmh.persistence.SearchResultItem;
37  import org.opencastproject.oaipmh.util.XmlGen;
38  import org.opencastproject.util.data.Function;
39  import org.opencastproject.util.data.Option;
40  
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.Collections;
47  import java.util.Date;
48  import java.util.List;
49  
50  import javax.xml.XMLConstants;
51  
52  /**
53   * OAI-PMH specific XML generator.
54   */
55  public abstract class OaiXmlGen extends XmlGen {
56  
57    protected OaiPmhRepository repository;
58  
59    /**
60     * Create a new OaiXmlGen for a certain repository.
61     */
62    public OaiXmlGen(OaiPmhRepository repository) {
63      super(some(OaiPmhConstants.OAI_2_0_XML_NS));
64      this.repository = repository;
65    }
66  
67    /**
68     * Create the OAI-PMH tag.
69     */
70    Element oai(Node... nodes) {
71      final List<Node> combined = new ArrayList<>();
72      combined.add(schemaLocation(OAI_2_0_SCHEMA_LOCATION));
73      combined.add($eTxt("responseDate", OAI_2_0_XML_NS, toUtcSecond(new Date())));
74      Collections.addAll(combined, nodes);
75      return $e("OAI-PMH",
76                Collections.singletonList(ns("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI)),
77                combined);
78    }
79  
80    /**
81     * Create the dublin core tag from single nodes.
82     */
83    Element dc(Node... nodes) {
84      List<Node> combined = new ArrayList<Node>(Arrays.asList(nodes));
85      combined.add(schemaLocation(OaiPmhConstants.OAI_DC_SCHEMA_LOCATION));
86      return $e("oai_dc:dc", OaiPmhConstants.OAI_DC_XML_NS,
87                Arrays.asList(ns("dc", "http://purl.org/dc/elements/1.1/"),
88                     ns("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI)),
89                combined);
90    }
91  
92    /**
93     * Create the dublin core tag from a search result item. Note: Sets are currently not supported.
94     */
95    @SuppressWarnings("unchecked") Element dc(final SearchResultItem item, Option<String> set) {
96      try {
97        return getDublincoreElement(item.getEpisodeDublinCore());
98      } catch (OaiPmhDatabaseException ex) {
99        return dc($e("dc:identifier", $txtBlank(item.getId())));
100     }
101   }
102 
103   // <dcterms:description xml:lang="en">
104   // Introduction lecture from the Institute for Atmospheric and Climate Science.
105   // </dcterms:description>
106   private Element getDublincoreElement(DublinCoreCatalog dc) {
107     List<Node> nodes = new ArrayList<Node>();
108     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_TITLE));
109     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_CREATOR));
110     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_SUBJECT));
111     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_DESCRIPTION));
112     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_PUBLISHER));
113     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_CONTRIBUTOR));
114     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_TYPE));
115     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_FORMAT));
116     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_IDENTIFIER));
117     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_SOURCE));
118     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_LANGUAGE));
119     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_RELATION));
120     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_COVERAGE));
121     nodes.addAll(getDublinCoreNodes(dc, DublinCore.PROPERTY_LICENSE));
122     return dc(nodes.toArray(new Node[nodes.size()]));
123   }
124 
125   private List<Node> getDublinCoreNodes(DublinCoreCatalog dc, EName eName) {
126     List<Node> nodes = new ArrayList<Node>();
127 
128     List<DublinCoreValue> values = dc.get(eName);
129     for (DublinCoreValue dcValue : values) {
130       Element element = $e("dc:" + eName.getLocalName(), $langNode(dcValue.getLanguage()), $txt(dcValue.getValue()));
131       nodes.add(element);
132     }
133     return nodes;
134   }
135 
136   /**
137    * Create the resumption token and store the query.
138    */
139   Node resumptionToken(final Option<String> resumptionToken, final String metadataPrefix, final SearchResult result,
140                        Date until, Option<String> set) {
141     // compute the token value...
142     final Option<Option<String>> token;
143     if (result.size() == result.getLimit()) {
144       SearchResultItem lastResult = result.getItems().get((int) (result.size() - 1));
145       // more to come...
146       token = some(some(repository.saveQuery(new ResumableQuery(metadataPrefix, lastResult.getModificationDate(),
147                                                                 until, set))));
148     } else if (resumptionToken.isSome()) {
149       // last page reached
150       token = some(Option.<String>none());
151     } else {
152       token = none();
153     }
154     // ... then transform it into a node
155     return token.map(new Function<Option<String>, Node>() {
156       @Override
157       public Node apply(Option<String> token) {
158         return $e("resumptionToken",
159                   // $a("completeListSize", Long.toString(result.getTotalSize())),
160                   // $a("cursor", Integer.toString(offset)),
161                   token.map(mkText).getOrElse(nodeZero));
162       }
163     }).getOrElse(nodeZero);
164   }
165 
166   /**
167    * Create a record element.
168    */
169   Element record(final SearchResultItem item, final Node metadata) {
170     if (item.isDeleted()) {
171       return $e("record", header(item));
172     } else {
173       return $e("record", header(item), $e("metadata", metadata));
174     }
175   }
176 
177   /**
178    * Create a metadata format element.
179    */
180   Element metadataFormat(MetadataFormat f) {
181     return $e("metadataFormat", $eTxt("metadataPrefix", f.getPrefix()), $eTxt("schema", f.getSchema().toString()),
182               $eTxt("metadataNamespace", f.getNamespace().toString()));
183   }
184 
185   /**
186    * Create a metadata prefix attribute if one is requested in the params.
187    */
188   Node metadataPrefixAttr(Params p) {
189     return $aSome("metadataPrefix", p.getMetadataPrefix());
190   }
191 
192   /**
193    * Create the header element for a result item.
194    */
195   Element header(final SearchResultItem item) {
196     // How to determine the media type?
197     // There is a field oc_mediatype in the index but this one distinguishes
198     // only audioVisual and series.
199     Element header = $e("header",
200                         $eTxt("identifier", item.getId()),
201                         $eTxt("datestamp", repository.toSupportedGranularity(item.getModificationDate())));
202     if (item.isDeleted()) {
203       header.setAttribute("status", "deleted");
204     }
205     for (String setSpec: item.getSetSpecs()) {
206       header.appendChild($eTxt("setSpec", setSpec));
207     }
208     return header;
209   }
210 
211   /**
212    * Merge two node arrays into a list.
213    */
214   protected List<Node> merge(Node[] a, Node... b) {
215     List<Node> merge = new ArrayList<Node>(a.length + b.length);
216     java.util.Collections.addAll(merge, a);
217     java.util.Collections.addAll(merge, b);
218     return merge;
219   }
220 }