1 | package eu.clarin.oai.provider.impl; |
---|
2 | |
---|
3 | import java.io.FilterOutputStream; |
---|
4 | import java.io.IOException; |
---|
5 | import java.io.OutputStream; |
---|
6 | import java.util.Date; |
---|
7 | import java.util.List; |
---|
8 | import java.util.Map; |
---|
9 | import java.util.TimeZone; |
---|
10 | |
---|
11 | import javax.xml.XMLConstants; |
---|
12 | import javax.xml.stream.XMLOutputFactory; |
---|
13 | import javax.xml.stream.XMLStreamException; |
---|
14 | import javax.xml.stream.XMLStreamWriter; |
---|
15 | |
---|
16 | import org.apache.commons.lang.time.FastDateFormat; |
---|
17 | |
---|
18 | import eu.clarin.oai.provider.MetadataFormat; |
---|
19 | import eu.clarin.oai.provider.OAIException; |
---|
20 | import eu.clarin.oai.provider.Record; |
---|
21 | import eu.clarin.oai.provider.Repository; |
---|
22 | import eu.clarin.oai.provider.ext.OAIOutputStream; |
---|
23 | import eu.clarin.oai.provider.ext.RepositoryAdapter; |
---|
24 | import eu.clarin.oai.provider.ext.ResumptionToken; |
---|
25 | import eu.clarin.oai.provider.ext.VerbContext; |
---|
26 | |
---|
27 | final class OAIOutputStreamImpl implements OAIOutputStream { |
---|
28 | private final static class FlushSkipOutputStream |
---|
29 | extends FilterOutputStream { |
---|
30 | private byte[] buf; |
---|
31 | private int bufCount = 0; |
---|
32 | |
---|
33 | public FlushSkipOutputStream(OutputStream out, int bufsize) { |
---|
34 | super(out); |
---|
35 | this.buf = new byte[(bufsize > 1024 ? bufsize : 1024)]; |
---|
36 | } |
---|
37 | |
---|
38 | @Override |
---|
39 | public synchronized void write(byte[] buffer, int offset, int length) |
---|
40 | throws IOException { |
---|
41 | if (buf == null) { |
---|
42 | throw new IOException("stream already closed"); |
---|
43 | } |
---|
44 | if (buffer == null) { |
---|
45 | throw new NullPointerException("buffer == null"); |
---|
46 | } |
---|
47 | if (offset < 0 || offset > buffer.length - length) { |
---|
48 | throw new ArrayIndexOutOfBoundsException( |
---|
49 | "offset out of bounds: " + offset); |
---|
50 | } |
---|
51 | if (length < 0) { |
---|
52 | throw new ArrayIndexOutOfBoundsException( |
---|
53 | "length out of bounds: " + length); |
---|
54 | } |
---|
55 | ensureCapacity(length); |
---|
56 | System.arraycopy(buffer, offset, buf, bufCount, length); |
---|
57 | bufCount += length; |
---|
58 | } |
---|
59 | |
---|
60 | @Override |
---|
61 | public synchronized void write(int b) throws IOException { |
---|
62 | if (buf == null) { |
---|
63 | throw new IOException("stream already closed"); |
---|
64 | } |
---|
65 | ensureCapacity(1); |
---|
66 | buf[bufCount++] = (byte) (b & 0xFF); |
---|
67 | } |
---|
68 | |
---|
69 | @Override |
---|
70 | public synchronized void close() throws IOException { |
---|
71 | if (buf == null) { |
---|
72 | return; |
---|
73 | } |
---|
74 | try { |
---|
75 | ensureCapacity(buf.length); // explicitly force flush |
---|
76 | super.close(); |
---|
77 | } finally { |
---|
78 | buf = null; |
---|
79 | } |
---|
80 | } |
---|
81 | |
---|
82 | @Override |
---|
83 | public synchronized void flush() throws IOException { |
---|
84 | // do nothing, defer flush() as long as possible |
---|
85 | } |
---|
86 | |
---|
87 | private void ensureCapacity(int needed) throws IOException { |
---|
88 | if (needed >= (buf.length - bufCount)) { |
---|
89 | out.write(buf, 0, bufCount); |
---|
90 | bufCount = 0; |
---|
91 | } |
---|
92 | } |
---|
93 | } // inner class FlushSkipOutputStream |
---|
94 | private static final String NS_OAI = |
---|
95 | "http://www.openarchives.org/OAI/2.0/"; |
---|
96 | private static final String NS_OAI_SCHEMA_LOCATION = |
---|
97 | "http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"; |
---|
98 | private static final String SCHEMA_LOCATION = |
---|
99 | NS_OAI + " " + NS_OAI_SCHEMA_LOCATION; |
---|
100 | private static final XMLOutputFactory writerFactory = |
---|
101 | XMLOutputFactory.newInstance(); |
---|
102 | private final RepositoryAdapter repository; |
---|
103 | private final OutputStream stream; |
---|
104 | private final XMLStreamWriter writer; |
---|
105 | |
---|
106 | OAIOutputStreamImpl(VerbContext ctx, OutputStream out) |
---|
107 | throws XMLStreamException { |
---|
108 | this.repository = ctx.getRepository(); |
---|
109 | this.stream = new FlushSkipOutputStream(out, 8192); |
---|
110 | writer = writerFactory.createXMLStreamWriter(stream, "utf-8"); |
---|
111 | writer.writeStartDocument("utf-8", "1.0"); |
---|
112 | |
---|
113 | // FIXME: make this a configuration option |
---|
114 | StringBuilder data = new StringBuilder(); |
---|
115 | data.append("type=\"text/xsl\" href=\""); |
---|
116 | data.append(ctx.getContextPath()); |
---|
117 | data.append("/oai2.xsl\""); |
---|
118 | writer.writeProcessingInstruction("xml-stylesheet", data.toString()); |
---|
119 | |
---|
120 | writer.setDefaultNamespace(NS_OAI); |
---|
121 | writer.writeStartElement("OAI-PMH"); |
---|
122 | writer.writeDefaultNamespace(NS_OAI); |
---|
123 | writer.writeNamespace("xsi", |
---|
124 | XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); |
---|
125 | writer.writeAttribute(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, |
---|
126 | "schemaLocation", SCHEMA_LOCATION); |
---|
127 | |
---|
128 | FastDateFormat fmt = getDateFormat(Repository.Granularity.SECONDS); |
---|
129 | writer.writeStartElement("responseDate"); |
---|
130 | writer.writeCharacters(fmt.format(System.currentTimeMillis())); |
---|
131 | writer.writeEndElement(); // responseDate element |
---|
132 | |
---|
133 | writer.writeStartElement("request"); |
---|
134 | if (ctx.getVerb() != null) { |
---|
135 | writer.writeAttribute("verb", ctx.getVerb()); |
---|
136 | Map<String, String> args = ctx.getUnparsedArguments(); |
---|
137 | for (Map.Entry<String, String> item : args.entrySet()) { |
---|
138 | writer.writeAttribute(item.getKey(), item.getValue()); |
---|
139 | } |
---|
140 | } |
---|
141 | writer.writeCharacters(ctx.getRequestURI()); |
---|
142 | writer.writeEndElement(); // request element |
---|
143 | } |
---|
144 | |
---|
145 | @Override |
---|
146 | public void close() throws IOException, XMLStreamException { |
---|
147 | writer.writeEndElement(); // OAI-PMH (root) element |
---|
148 | writer.writeEndDocument(); |
---|
149 | writer.flush(); |
---|
150 | writer.close(); |
---|
151 | // explicitly close output stream, as XMLStreamWriter does not! |
---|
152 | stream.close(); |
---|
153 | } |
---|
154 | |
---|
155 | @Override |
---|
156 | public void flush() throws XMLStreamException { |
---|
157 | writer.flush(); |
---|
158 | } |
---|
159 | |
---|
160 | @Override |
---|
161 | public XMLStreamWriter getXMLStreamWriter() throws XMLStreamException { |
---|
162 | return writer; |
---|
163 | } |
---|
164 | |
---|
165 | @Override |
---|
166 | public void writeStartElement(String localName) throws XMLStreamException { |
---|
167 | writer.writeStartElement(localName); |
---|
168 | } |
---|
169 | |
---|
170 | @Override |
---|
171 | public void writeStartElement(String namespaceURI, String localName) |
---|
172 | throws XMLStreamException { |
---|
173 | writer.writeStartElement(namespaceURI, localName); |
---|
174 | } |
---|
175 | |
---|
176 | @Override |
---|
177 | public void writeStartElement(String namespaceURI, String localName, |
---|
178 | List<NamespaceDecl> decls) throws XMLStreamException { |
---|
179 | for (NamespaceDecl decl : decls) { |
---|
180 | writer.setPrefix(decl.getPrefix(), decl.getNamespaceURI()); |
---|
181 | } |
---|
182 | writer.writeStartElement(namespaceURI, localName); |
---|
183 | boolean schemaDeclWritten = false; |
---|
184 | for (NamespaceDecl decl : decls) { |
---|
185 | writer.writeNamespace(decl.getPrefix(), decl.getNamespaceURI()); |
---|
186 | if (decl.hasSchemaLocation()) { |
---|
187 | /* |
---|
188 | * From an XML point of view, the XSI-namespace is still in |
---|
189 | * scope and this is not needed, but all other providers show |
---|
190 | * this behavior. |
---|
191 | */ |
---|
192 | if (!schemaDeclWritten) { |
---|
193 | writer.writeNamespace("xsi", |
---|
194 | XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); |
---|
195 | schemaDeclWritten = true; |
---|
196 | } |
---|
197 | writer.writeAttribute( |
---|
198 | XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, |
---|
199 | "schemaLocation", |
---|
200 | decl.getNamespaceURI() + " " + decl.getSchemaLocation()); |
---|
201 | } |
---|
202 | } |
---|
203 | } |
---|
204 | |
---|
205 | @Override |
---|
206 | public void writeEndElement() throws XMLStreamException { |
---|
207 | writer.writeEndElement(); |
---|
208 | } |
---|
209 | |
---|
210 | @Override |
---|
211 | public void writeAttribute(String localName, String value) |
---|
212 | throws XMLStreamException { |
---|
213 | writer.writeAttribute(localName, value); |
---|
214 | } |
---|
215 | |
---|
216 | @Override |
---|
217 | public void writeAttribute(String namespaceURI, String localName, |
---|
218 | String value) throws XMLStreamException { |
---|
219 | writer.writeAttribute(namespaceURI, localName, value); |
---|
220 | } |
---|
221 | |
---|
222 | @Override |
---|
223 | public void writeCharacters(String text) throws XMLStreamException { |
---|
224 | writer.writeCharacters(text); |
---|
225 | } |
---|
226 | |
---|
227 | @Override |
---|
228 | public void writeDate(Date date) throws XMLStreamException { |
---|
229 | FastDateFormat fmt = getDateFormat(repository.getGranularity()); |
---|
230 | writer.writeCharacters(fmt.format(date)); |
---|
231 | } |
---|
232 | |
---|
233 | @Override |
---|
234 | public void writeRecordHeader(Record record) throws XMLStreamException { |
---|
235 | writer.writeStartElement("header"); |
---|
236 | if (record.isDeleted()) { |
---|
237 | writer.writeAttribute("status", "deleted"); |
---|
238 | } |
---|
239 | writer.writeStartElement("identifier"); |
---|
240 | writer.writeCharacters(repository.createRecordId(record.getLocalId())); |
---|
241 | writer.writeEndElement(); // identifier element |
---|
242 | writer.writeStartElement("datestamp"); |
---|
243 | writeDate(record.getDatestamp()); |
---|
244 | writer.writeEndElement(); // datestamp element |
---|
245 | List<String> setSpecs = record.getSetSpecs(); |
---|
246 | if (setSpecs != null) { |
---|
247 | for (String setSpec : setSpecs) { |
---|
248 | writer.writeStartElement("setSpec"); |
---|
249 | writer.writeCharacters(setSpec); |
---|
250 | writer.writeEndElement(); // setSpec element |
---|
251 | } |
---|
252 | } |
---|
253 | writer.writeEndElement(); // header element |
---|
254 | } |
---|
255 | |
---|
256 | @Override |
---|
257 | public void writeRecord(Record record, MetadataFormat format) |
---|
258 | throws XMLStreamException, OAIException { |
---|
259 | writer.writeStartElement("record"); |
---|
260 | writeRecordHeader(record); |
---|
261 | if (!record.isDeleted()) { |
---|
262 | writer.writeStartElement("metadata"); |
---|
263 | // FIXME: re-work! |
---|
264 | record.writeRecord(writer, format); |
---|
265 | writer.writeEndElement(); // metadata element |
---|
266 | } |
---|
267 | writer.writeEndElement(); // record element |
---|
268 | } |
---|
269 | |
---|
270 | @Override |
---|
271 | public void writeResumptionToken(ResumptionToken token) |
---|
272 | throws XMLStreamException { |
---|
273 | writer.writeStartElement("resumptionToken"); |
---|
274 | if (token != null) { |
---|
275 | FastDateFormat fmt = getDateFormat(Repository.Granularity.SECONDS); |
---|
276 | writer.writeAttribute("expirationDate", |
---|
277 | fmt.format(token.getExpirationDate())); |
---|
278 | if (token.getCursor() >= 0) { |
---|
279 | writer.writeAttribute("cursor", |
---|
280 | Integer.toString(token.getCursor())); |
---|
281 | } |
---|
282 | if (token.getCompleteListSize() > 0) { |
---|
283 | writer.writeAttribute("completeListSize", |
---|
284 | Integer.toString(token.getCompleteListSize())); |
---|
285 | } |
---|
286 | writer.writeCharacters(Long.toString(token.getId())); |
---|
287 | } |
---|
288 | writer.writeEndElement(); // resumptionToken element |
---|
289 | } |
---|
290 | |
---|
291 | private FastDateFormat getDateFormat(Repository.Granularity g) { |
---|
292 | switch (g) { |
---|
293 | case DAYS: |
---|
294 | return FastDateFormat.getInstance("yyyy-MM-dd", |
---|
295 | TimeZone.getTimeZone("UTC")); |
---|
296 | default: |
---|
297 | return FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", |
---|
298 | TimeZone.getTimeZone("UTC")); |
---|
299 | } // switch |
---|
300 | } |
---|
301 | |
---|
302 | } // class OAIOutputStreamImpl |
---|