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 						// <d:fileset>
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 							// insert entries
227 							((BufferedXMLStreamWriter)result).writeEvent(added);
228 					} else if (depth == 1) {
229 						// <d:file>
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 								// skip entry
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 									// update entry
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 									// keep entry
272 									XMLStreamWriterHelper.writeElement(result, source);
273 							}
274 						} else
275 							// keep entry
276 							XMLStreamWriterHelper.writeElement(result, source);
277 					} else
278 						// keep entry
279 						XMLStreamWriterHelper.writeElement(result, source);
280 					break;
281 				case END_ELEMENT:
282 					depth--;
283 					if (depth == 0) {
284 						// </d:fileset>
285 						if (!first)
286 							// insert entries
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; // absolute
329 		URI href; // relative or absolute
330 		URI originalHref = null; // absolute
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 }