1 package org.daisy.pipeline.fileset.calabash.impl;
2
3 import java.net.URI;
4 import java.util.ArrayList;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.NoSuchElementException;
9
10 import javax.xml.namespace.QName;
11 import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
12 import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
13 import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT;
14 import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
15 import javax.xml.stream.XMLStreamException;
16 import javax.xml.stream.XMLStreamWriter;
17
18 import com.xmlcalabash.core.XProcRuntime;
19 import com.xmlcalabash.io.ReadablePipe;
20 import com.xmlcalabash.io.WritablePipe;
21 import com.xmlcalabash.library.DefaultStep;
22 import com.xmlcalabash.model.RuntimeValue;
23 import com.xmlcalabash.runtime.XAtomicStep;
24
25 import net.sf.saxon.s9api.XdmItem;
26 import net.sf.saxon.s9api.XdmNode;
27 import net.sf.saxon.s9api.SaxonApiException;
28
29 import org.daisy.common.saxon.SaxonHelper;
30 import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
31 import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
32 import org.daisy.common.stax.XMLStreamWriterHelper;
33 import org.daisy.common.stax.XMLStreamWriterHelper.BufferedXMLStreamWriter;
34 import org.daisy.common.stax.XMLStreamWriterHelper.FutureWriterEvent;
35 import org.daisy.common.transform.TransformerException;
36 import org.daisy.common.xproc.calabash.XProcStep;
37 import org.daisy.common.xproc.calabash.XProcStepProvider;
38 import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
39 import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
40 import org.daisy.common.xproc.XProcMonitor;
41 import org.daisy.pipeline.file.FileUtils;
42 import org.daisy.pipeline.fileset.Fileset;
43
44 import org.osgi.service.component.annotations.Component;
45
46 import org.slf4j.Logger;
47
48 public class AddEntriesStep extends DefaultStep implements XProcStep {
49
50 @Component(
51 name = "pxi:fileset-add-entries",
52 service = { XProcStepProvider.class },
53 property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}fileset-add-entries" }
54 )
55 public static class StepProvider implements XProcStepProvider {
56 @Override
57 public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
58 return new AddEntriesStep(runtime, step);
59 }
60 }
61
62 private static final net.sf.saxon.s9api.QName _FIRST = new net.sf.saxon.s9api.QName("first");
63 private static final net.sf.saxon.s9api.QName _REPLACE = new net.sf.saxon.s9api.QName("replace");
64 private static final net.sf.saxon.s9api.QName _REPLACE_ATTRIBUTES = new net.sf.saxon.s9api.QName("replace-attributes");
65 private static final net.sf.saxon.s9api.QName _ASSERT_SINGLE_ENTRY = new net.sf.saxon.s9api.QName("assert-single-entry");
66
67 private ReadablePipe sourceFilesetPipe = null;
68 private ReadablePipe sourceInMemoryPipe = null;
69 private ReadablePipe entriesPipe = null;
70 private WritablePipe resultFilesetPipe = null;
71 private WritablePipe resultInMemoryPipe = null;
72 private Map<QName,String> fileAttributes = null;
73
74 private AddEntriesStep(XProcRuntime runtime, XAtomicStep step) {
75 super(runtime, step);
76 }
77
78 @Override
79 public void setInput(String port, ReadablePipe pipe) {
80 if ("source.fileset".equals(port))
81 sourceFilesetPipe = pipe;
82 else if ("source.in-memory".equals(port))
83 sourceInMemoryPipe = pipe;
84 else if ("entries".equals(port))
85 entriesPipe = pipe;
86 }
87
88 @Override
89 public void setOutput(String port, WritablePipe pipe) {
90 if ("result.fileset".equals(port))
91 resultFilesetPipe = pipe;
92 else if ("result.in-memory".equals(port))
93 resultInMemoryPipe = pipe;
94 }
95
96 @Override
97 public void reset() {
98 sourceFilesetPipe.resetReader();
99 sourceInMemoryPipe.resetReader();
100 entriesPipe.resetReader();
101 resultFilesetPipe.resetWriter();
102 resultInMemoryPipe.resetWriter();
103 if (fileAttributes != null) fileAttributes.clear();
104 }
105
106 public void setParameter(net.sf.saxon.s9api.QName name, RuntimeValue value) {
107 if (fileAttributes == null)
108 fileAttributes = new HashMap<>();
109 fileAttributes.put(SaxonHelper.jaxpQName(name), value.getString());
110 }
111
112 public void setParameter(String port, net.sf.saxon.s9api.QName name, RuntimeValue value) {
113 setParameter(name, value);
114 }
115
116 @Override
117 public void run() throws SaxonApiException {
118 super.run();
119 try {
120 List<File> entries; {
121 entries = new ArrayList<>();
122 boolean assertSingleEntry = getOption(_ASSERT_SINGLE_ENTRY, false);
123 RuntimeValue href = getOption(new net.sf.saxon.s9api.QName(Fileset.XMLConstants._HREF));
124 if (href != null)
125 for (XdmItem i : href.getValue())
126 entries.add(new File(URI.create(i.getStringValue())));
127 if (entriesPipe.moreDocuments()) {
128 if (!entries.isEmpty())
129 throw TransformerException.wrap(
130 new IllegalArgumentException(
131 assertSingleEntry
132 ? "Expected 0 documents on the entry port"
133 : "Expected 0 documents on the entries port"));
134 entries.add(new File(entriesPipe.read()));
135 if (entriesPipe.moreDocuments()) {
136 if (assertSingleEntry)
137 throw TransformerException.wrap(
138 new IllegalArgumentException("Expected exactly 1 document on the entry port (got more)"));
139 entries.add(new File(entriesPipe.read()));
140 while (entriesPipe.moreDocuments())
141 entries.add(new File(entriesPipe.read()));
142 }
143 } else if (assertSingleEntry && entries.isEmpty())
144 throw TransformerException.wrap(
145 new IllegalArgumentException("Expected 1 document on the entry port (got 0)"));
146 }
147 URI originalHref; {
148 String option = getOption(new net.sf.saxon.s9api.QName(Fileset.XMLConstants._ORIGINAL_HREF), "");
149 if ("".equals(option))
150 originalHref = null;
151 else
152 originalHref = URI.create(option);
153 }
154 String mediaType = getOption(new net.sf.saxon.s9api.QName(Fileset.XMLConstants._MEDIA_TYPE), "");
155 if ("".equals(mediaType)) mediaType = null;
156 for (File f : entries) {
157 f.originalHref = originalHref;
158 f.mediaType = mediaType;
159 f.otherAttributes = fileAttributes;
160 }
161 boolean first = getOption(_FIRST, false);
162 boolean replace = getOption(_REPLACE, false);
163 boolean replaceAttributes = getOption(_REPLACE_ATTRIBUTES, false);
164 List<File> added = addEntries(new XMLCalabashInputValue(sourceFilesetPipe).asXMLStreamReader(),
165 new XMLCalabashOutputValue(resultFilesetPipe, runtime).asXMLStreamWriter(),
166 entries, first, replace, replaceAttributes, logger);
167 if (first)
168 for (File f : added)
169 if (f.node != null)
170 resultInMemoryPipe.write(f.node);
171 while (sourceInMemoryPipe.moreDocuments())
172 resultInMemoryPipe.write(sourceInMemoryPipe.read());
173 if (!first)
174 for (File f : added)
175 if (f.node != null)
176 resultInMemoryPipe.write(f.node);
177 } catch (Throwable e) {
178 throw XProcStep.raiseError(e, step);
179 }
180 }
181
182 private static List<File> addEntries(BaseURIAwareXMLStreamReader source, BaseURIAwareXMLStreamWriter result,
183 List<File> entries, boolean first, boolean replace, boolean replaceAttributes, Logger logger)
184 throws XMLStreamException {
185 FileSet added = new FileSet();
186 added.addAll(entries);
187 URI filesetBase = source.getBaseURI();
188 result.setBaseURI(filesetBase);
189 result.writeStartDocument();
190 result = new BufferedXMLStreamWriter(result);
191 int depth = 0;
192 boolean hasXmlBase = false;
193 document: while (true)
194 try {
195 int event = source.next();
196 switch (event) {
197 case START_DOCUMENT:
198 break;
199 case END_DOCUMENT:
200 break document;
201 case START_ELEMENT:
202 if (entries.isEmpty())
203 XMLStreamWriterHelper.writeElement(result, source);
204 else if (depth == 0) {
205
206 for (int i = 0; i < source.getAttributeCount(); i++)
207 if (Fileset.XMLConstants.XML_BASE.equals(source.getAttributeName(i))) {
208 hasXmlBase = true;
209 filesetBase = filesetBase.resolve(source.getAttributeValue(i));
210 break;
211 }
212 filesetBase = FileUtils.normalizeURI(filesetBase);
213 for (File f : added) {
214 f.base = FileUtils.normalizeURI(filesetBase.resolve(f.href));
215 if (hasXmlBase)
216 f.href = FileUtils.relativizeURI(f.base, filesetBase);
217 else if (!f.href.isAbsolute())
218 logger.warn("Adding a relative resource to a file set with no base directory");
219 if (f.originalHref != null)
220 f.originalHref = FileUtils.normalizeURI(filesetBase.resolve(f.originalHref));
221 }
222 XMLStreamWriterHelper.writeStartElement(result, source.getName());
223 XMLStreamWriterHelper.writeAttributes(result, source);
224 depth++;
225 if (first)
226
227 ((BufferedXMLStreamWriter)result).writeEvent(added);
228 } else if (depth == 1) {
229
230 File match = null; {
231 for (int i = 0; i < source.getAttributeCount(); i++)
232 if (Fileset.XMLConstants._HREF.equals(source.getAttributeName(i))) {
233 URI base = FileUtils.normalizeURI(filesetBase.resolve(source.getAttributeValue(i)));
234 for (File f : entries)
235 if (f.base.equals(base)) {
236 match = f;
237 break;
238 }
239 break;
240 }
241 }
242 if (match != null) {
243 entries.remove(match);
244 if (replace) {
245
246 element: while (true) {
247 event = source.next();
248 switch (event) {
249 case START_ELEMENT:
250 depth++;
251 break;
252 case END_ELEMENT:
253 if (depth == 1) break element;
254 depth--;
255 break;
256 default:
257 }
258 }
259 } else {
260 added.remove(match);
261 if (replaceAttributes) {
262
263 XMLStreamWriterHelper.writeStartElement(result, source.getName());
264 Map<QName,String> existingAttributes = new HashMap<>(); {
265 for (int i = 0; i < source.getAttributeCount(); i++)
266 existingAttributes.put(source.getAttributeName(i), source.getAttributeValue(i));
267 }
268 writeFileAttributes(result, existingAttributes, match.originalHref, match.mediaType, match.otherAttributes);
269 depth++;
270 } else
271
272 XMLStreamWriterHelper.writeElement(result, source);
273 }
274 } else
275
276 XMLStreamWriterHelper.writeElement(result, source);
277 } else
278
279 XMLStreamWriterHelper.writeElement(result, source);
280 break;
281 case END_ELEMENT:
282 depth--;
283 if (depth == 0) {
284
285 if (!first)
286
287 ((BufferedXMLStreamWriter)result).writeEvent(added);
288 }
289 default:
290 XMLStreamWriterHelper.writeEvent(result, source);
291 }
292 } catch (NoSuchElementException e) {
293 break;
294 }
295 result.writeEndDocument();
296 added.ready = true;
297 result.flush();
298 return added;
299 }
300
301 private static void writeFileAttributes(XMLStreamWriter result, Map<QName,String> existingAttributes,
302 URI originalHref, String mediaType, Map<QName,String> otherAttributes)
303 throws XMLStreamException {
304 if (originalHref != null)
305 XMLStreamWriterHelper.writeAttribute(result, Fileset.XMLConstants._ORIGINAL_HREF, originalHref.toASCIIString());
306 if (mediaType != null)
307 XMLStreamWriterHelper.writeAttribute(result, Fileset.XMLConstants._MEDIA_TYPE, mediaType);
308 if (otherAttributes != null)
309 for (QName attr : otherAttributes.keySet())
310 if (Fileset.XMLConstants._HREF.equals(attr) ||
311 Fileset.XMLConstants._ORIGINAL_HREF.equals(attr) ||
312 Fileset.XMLConstants._MEDIA_TYPE.equals(attr))
313 throw TransformerException.wrap(
314 new IllegalArgumentException(
315 "href, original-href and media-type are not allowed file attributes"));
316 else
317 XMLStreamWriterHelper.writeAttribute(result, attr, otherAttributes.get(attr));
318 if (existingAttributes != null)
319 for (QName attr : existingAttributes.keySet())
320 if ((originalHref == null || !Fileset.XMLConstants._ORIGINAL_HREF.equals(attr)) &&
321 (mediaType == null || !Fileset.XMLConstants._MEDIA_TYPE.equals(attr)) &&
322 (otherAttributes == null || !otherAttributes.containsKey(attr)))
323 XMLStreamWriterHelper.writeAttribute(result, attr, existingAttributes.get(attr));
324 }
325
326 private static class File {
327 XdmNode node;
328 URI base = null;
329 URI href;
330 URI originalHref = null;
331 String mediaType = null;
332 Map<QName,String> otherAttributes = null;
333 public File(URI href) {
334 this.node = null;
335 this.href = href;
336 }
337 public File(XdmNode node) {
338 this.node = node;
339 this.href = node.getBaseURI();
340 }
341 }
342
343 @SuppressWarnings("serial")
344 private static class FileSet extends ArrayList<File> implements FutureWriterEvent {
345 boolean ready = false;
346 public void writeTo(XMLStreamWriter writer) throws XMLStreamException {
347 for (File f : this) {
348 XMLStreamWriterHelper.writeStartElement(writer, Fileset.XMLConstants.D_FILE);
349 XMLStreamWriterHelper.writeAttribute(writer, Fileset.XMLConstants._HREF, f.href.toASCIIString());
350 writeFileAttributes(writer, null, f.originalHref, f.mediaType, f.otherAttributes);
351 writer.writeEndElement();
352 }
353 }
354 public boolean isReady() {
355 return ready;
356 }
357 }
358 }