1   package org.daisy.pipeline.fileset;
2   
3   import java.io.ByteArrayInputStream;
4   import java.io.FileInputStream;
5   import java.io.FileNotFoundException;
6   import java.io.FileOutputStream;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.OutputStream;
10  import java.net.URI;
11  import java.util.ArrayList;
12  import java.util.List;
13  import java.util.NoSuchElementException;
14  import java.util.Optional;
15  
16  import javax.xml.namespace.QName;
17  import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
18  import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
19  import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT;
20  import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
21  import javax.xml.stream.XMLStreamException;
22  import javax.xml.stream.XMLStreamWriter;
23  
24  import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
25  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
26  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
27  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
28  import static org.daisy.pipeline.file.FileUtils.normalizeURI;
29  import static org.daisy.pipeline.file.FileUtils.relativizeURI;
30  
31  public class Fileset {
32  	private Fileset() {}
33  
34  	/**
35  	 * Java counterpart of d:file.
36  	 *
37  	 * This class is immutable.
38  	 */
39  	public static class File {
40  
41  		public final URI href;
42  		public final Optional<String> mediaType;
43  
44  		private final FileData data;
45  
46  		/**
47  		 * @param href shoud be normalized
48  		 */
49  		private File(FileData data, URI href, Optional<String> mediaType) {
50  			this.data = data;
51  			this.href = href;
52  			this.mediaType = mediaType;
53  		}
54  
55  		/**
56  		 * @param from For now this must be an absolute, hierarchical URI with a scheme equal to
57  		 *             "file", a non-empty path component, and undefined authority, query, and
58  		 *             fragment components.
59  		 * @throws IllegalArgumentException if the URI does not point to a file on disk.
60  		 */
61  		public static File load(URI from, Optional<String> mediaType) {
62  			return load(from, from, mediaType);
63  		}
64  
65  		public static File load(URI from, URI href, Optional<String> mediaType) {
66  			return load(new java.io.File(from), href, mediaType);
67  		}
68  
69  		public static File load(java.io.File from, Optional<String> mediaType) {
70  			return load(from, from.toURI(), mediaType);
71  		}
72  
73  		public static File load(java.io.File from, URI href, Optional<String> mediaType) {
74  			return new File(new FileDataOnDisk(from), normalizeURI(href), mediaType);
75  		}
76  
77  		public static File load(byte[] data, URI href, Optional<String> mediaType) {
78  			return new File(new FileDataInMemory(data), normalizeURI(href), mediaType);
79  		}
80  
81  		public InputStream read() throws IOException {
82  			return data.read();
83  		}
84  
85  		public File copy(URI href) {
86  			return new File(data, normalizeURI(href), mediaType);
87  		}
88  
89  		/**
90  		 * Store file to disk
91  		 *
92  		 * @throws IOException if "href" does not point to a file on disk or if the file can not be
93  		 *                     written to.
94  		 */
95  		public File store() throws IOException {
96  			java.io.File file; {
97  				try {
98  					file = new java.io.File(href);
99  				} catch (IllegalArgumentException e) {
100 					throw new IOException("href is not a file URI: " + href);
101 				}
102 			}
103 			return new File(data.store(file), href, mediaType);
104 		}
105 	}
106 
107 	private static abstract class FileData {
108 
109 		public abstract InputStream read() throws IOException;
110 
111 		public FileData store(java.io.File destination) throws IOException {
112 			try (OutputStream os = new FileOutputStream(destination)) {
113 				try (InputStream is = read()) {
114 					byte[] bucket = new byte[1024];
115 					int bytesRead = 0;
116 					while ((bytesRead = is.read(bucket, 0, bucket.length)) != -1) {
117 						os.write(bucket, 0, bytesRead);
118 					}
119 				}
120 			}
121 			return new FileDataOnDisk(destination);
122 		}
123 	}
124 
125 	private static class FileDataOnDisk extends FileData {
126 
127 		private final java.io.File file;
128 
129 		/**
130 		 * @param file is read lazily, but we already check whether it exists
131 		 */
132 		private FileDataOnDisk(java.io.File file) {
133 			if (!(file.isFile() && file.exists()))
134 				throw new IllegalArgumentException(new FileNotFoundException("file does not exist: " + file));
135 			this.file = file;
136 		}
137 
138 		public InputStream read() throws IOException {
139 			return new FileInputStream(file);
140 		}
141 
142 		@Override
143 		public FileData store(java.io.File destination) throws IOException {
144 			if (destination.equals(file))
145 				return this;
146 			return super.store(destination);
147 		}
148 	}
149 
150 	private static class FileDataInMemory extends FileData {
151 
152 		private final byte[] data;
153 
154 		/**
155 		 * @param data is read lazily
156 		 */
157 		private FileDataInMemory(byte[] data) {
158 			this.data = data;
159 		}
160 
161 		public InputStream read() {
162 			return new ByteArrayInputStream(data);
163 		}
164 	}
165 
166 	public static List<File> unmarshall(BaseURIAwareXMLStreamReader xml) throws XMLStreamException {
167 		List<File> fileset = new ArrayList<>();
168 		URI filesetBase = xml.getBaseURI();
169 		int depth = 0;
170 	  document: while (true) {
171 			try {
172 				int event = xml.next();
173 				switch (event) {
174 				case START_DOCUMENT:
175 					break;
176 				case END_DOCUMENT:
177 					break document;
178 				case START_ELEMENT:
179 					if (depth == 0 && XMLConstants.D_FILESET.equals(xml.getName())) {
180 						for (int i = 0; i < xml.getAttributeCount(); i++)
181 							if (XMLConstants.XML_BASE.equals(xml.getAttributeName(i))) {
182 								filesetBase = filesetBase.resolve(xml.getAttributeValue(i));
183 								break;
184 							}
185 						depth++;
186 						break;
187 					} else if (depth == 1 && XMLConstants.D_FILE.equals(xml.getName())) {
188 						URI href = null;
189 						URI originalHref = null;
190 						String mediaType = null;
191 						for (int i = 0; i < xml.getAttributeCount(); i++)
192 							if (XMLConstants._HREF.equals(xml.getAttributeName(i)))
193 								href = filesetBase.resolve(xml.getAttributeValue(i));
194 							else if (XMLConstants._ORIGINAL_HREF.equals(xml.getAttributeName(i)))
195 								originalHref = filesetBase.resolve(xml.getAttributeValue(i));
196 							else if (XMLConstants._MEDIA_TYPE.equals(xml.getAttributeName(i)))
197 								mediaType = xml.getAttributeValue(i);
198 						if (href != null) {
199 							if (originalHref != null)
200 								fileset.add(File.load(originalHref, href, Optional.ofNullable(mediaType)));
201 							else
202 								fileset.add(File.load(href, Optional.ofNullable(mediaType)));
203 						}
204 					}
205 					{ // consume whole element
206 						int d = depth + 1;
207 					  element: while (true) {
208 							event = xml.next();
209 							switch (event) {
210 							case START_ELEMENT:
211 								d++;
212 								break;
213 							case END_ELEMENT:
214 								d--;
215 								if (d == depth) break element;
216 							default:
217 							}
218 						}
219 					}
220 					break;
221 				case END_ELEMENT:
222 					depth--;
223 					break;
224 				default:
225 				}
226 			} catch (NoSuchElementException e) {
227 				break;
228 			}
229 		}
230 		return fileset;
231 	}
232 
233 	/**
234 	 * @param xmlBase if not <code>null</code>, use this to create a <code>xml:base</code> attribute
235 	 *                and to relativize <code>href</code> attributes against. It is up to the caller
236 	 *                to set the base URI on the document (see {@link BaseURIAwareXMLStreamWriter}).
237 	 */
238 	public static void marshall(XMLStreamWriter xml, URI xmlBase, List<File> fileset) throws XMLStreamException {
239 		xml.writeStartDocument();
240 		writeStartElement(xml, XMLConstants.D_FILESET);
241 		if (xmlBase != null)
242 			writeAttribute(xml, XMLConstants.XML_BASE, xmlBase.toASCIIString());
243 		for (File file : fileset) {
244 			writeStartElement(xml, XMLConstants.D_FILE);
245 			writeAttribute(xml,
246 			               XMLConstants._HREF,
247 			               (xmlBase != null ? relativizeURI(file.href, xmlBase) : file.href).toASCIIString());
248 			if (file.mediaType.isPresent())
249 				writeAttribute(xml, XMLConstants._MEDIA_TYPE, file.mediaType.get());
250 			if (file.data instanceof FileDataOnDisk)
251 				writeAttribute(xml,
252 				               XMLConstants._ORIGINAL_HREF,
253 				               ((FileDataOnDisk)file.data).file.toURI().toASCIIString());
254 			xml.writeEndElement();
255 		}
256 		xml.writeEndElement();
257 		xml.writeEndDocument();
258 	}
259 
260 	public static final class XMLConstants {
261 		private XMLConstants() {}
262 
263 		public static final QName D_FILESET = new QName("http://www.daisy.org/ns/pipeline/data", "fileset", "d");
264 		public static final QName XML_BASE = new QName(javax.xml.XMLConstants.XML_NS_URI, "base", "xml");
265 		public static final QName D_FILE = new QName("http://www.daisy.org/ns/pipeline/data", "file", "d");
266 		public static final QName _MEDIA_TYPE = new QName("media-type");
267 		public static final QName _HREF = new QName("href");
268 		public static final QName _ORIGINAL_HREF = new QName("original-href");
269 	}
270 }