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