1   package org.daisy.pipeline.file.calabash.impl;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.FileOutputStream;
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.io.InputStreamReader;
9   import java.io.OutputStreamWriter;
10  import java.io.Reader;
11  import java.io.UnsupportedEncodingException;
12  import java.io.Writer;
13  import java.net.URI;
14  import java.nio.file.Files;
15  import java.nio.file.StandardCopyOption;
16  import java.util.Map;
17  
18  import net.sf.saxon.s9api.QName;
19  import net.sf.saxon.s9api.SaxonApiException;
20  
21  import org.daisy.common.xproc.calabash.XProcStep;
22  import org.daisy.common.xproc.calabash.XProcStepProvider;
23  import org.daisy.common.xproc.XProcMonitor;
24  
25  import org.slf4j.Logger;
26  
27  import com.xmlcalabash.core.XProcConstants;
28  import com.xmlcalabash.core.XProcException;
29  import com.xmlcalabash.core.XProcRuntime;
30  import com.xmlcalabash.io.WritablePipe;
31  import com.xmlcalabash.library.DefaultStep;
32  import com.xmlcalabash.model.RuntimeValue;
33  import com.xmlcalabash.runtime.XAtomicStep;
34  import com.xmlcalabash.util.TreeWriter;
35  
36  import org.osgi.service.component.annotations.Component;
37  
38  @Component(
39  	name = "px:set-doctype",
40  	service = { XProcStepProvider.class },
41  	property = { "type:String={http://www.daisy.org/ns/pipeline/xproc}set-doctype" }
42  )
43  public class SetDoctypeProvider implements XProcStepProvider {
44  
45  	@Override
46  	public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
47  		return new SetDoctype(runtime, step);
48  	}
49  
50  	public static class SetDoctype extends DefaultStep implements XProcStep {
51  		private static final QName _href = new QName("href");
52  		private static final QName _doctype = new QName("doctype");
53  		private static final QName _encoding = new QName("encoding");
54  
55  		private WritablePipe result = null;
56  
57  		public SetDoctype(XProcRuntime runtime, XAtomicStep step) {
58  			super(runtime,step);
59  		}
60  
61  		public void setOutput(String port, WritablePipe pipe) {
62  			result = pipe;
63  		}
64  
65  		public void reset() {
66  			result.resetWriter();
67  		}
68  
69  		public void run() throws SaxonApiException {
70  			super.run();
71  
72  			String doctype = getOption(_doctype, "");
73  			String encoding = getOption(_encoding, "utf-8");
74  
75  			RuntimeValue href = getOption(_href);
76  			URI sourceUri = href.getBaseURI().resolve(href.getString());
77  			File file = new File(sourceUri.getPath());
78  			
79  			if (file.isDirectory()) {
80  				throw new XProcException(step, "Cannot set doctype of file: file is a directory: " + file.getAbsolutePath());
81  			}
82  			
83  			boolean ok = false;
84  			ok = SetDoctype.setDoctype(file, encoding, doctype, logger);
85  
86  
87  			TreeWriter tree = new TreeWriter(runtime);
88  			tree.startDocument(step.getNode().getBaseURI());
89  			tree.addStartElement(XProcConstants.c_result);
90  			tree.startContent();
91  			
92  			if (ok) {
93  				logger.debug("SetDoctype: successfully set the doctype");
94  				tree.addText(file.toURI().toString());
95  			} else {
96  				String errorMessage = "px:set-doctype failed to read from "+file+" (doctype: "+doctype+", filesize: "+(file==null?'?':file.length())+")";
97  				logger.warn("SetDoctype: "+errorMessage);
98  				tree.addAttribute(new QName("error"), errorMessage);
99  			}
100 
101 			tree.addEndElement();
102 			tree.endDocument();
103 
104 			result.write(tree.getResult());
105 		}
106 		
107 		public static boolean setDoctype(File file, String encoding, String doctype, Logger logger) {
108 			
109 			logger.debug("SetDoctype: file=["+file+"]");
110 			logger.debug("SetDoctype: encoding=["+encoding+"]");
111 			logger.debug("SetDoctype: doctype=["+doctype+"]");
112 			
113 			Reader reader = null;
114 			InputStream in = null;
115 			
116 			File tempFile = null;
117 			Writer writer = null;
118 			
119 			boolean success = false;
120 			
121 			try {
122 				in = new FileInputStream(file);
123 				try {
124 					reader = new InputStreamReader(in, encoding);
125 				} catch (UnsupportedEncodingException e) {
126 					reader = new InputStreamReader(in);
127 				}
128 				
129 				tempFile = File.createTempFile("setDoctype", "");
130 				writer = new OutputStreamWriter(new FileOutputStream(tempFile), encoding);
131 				
132 				success = setDoctypeOnStream(reader, writer, doctype, file.toString(), logger);
133 				try {
134 					writer.close();
135 				} catch (IOException e) {
136 					logger.error("SetDoctype: unable to close OutputStreamWriter", e);
137 				}
138 
139 				try {
140 					reader.close();
141 				} catch (IOException e) {
142 					logger.error("SetDoctype: unable to close InputStreamReader", e);
143 				}
144 				
145 				if (success) {
146 				    Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
147 				}
148 				
149 			} catch (IOException e) {
150 				if (logger != null) {
151 					logger.error("SetDoctype: px:set-doctype failed to read from "+file, e);
152 				}
153 				e.printStackTrace();
154 
155 			} finally {
156 				if (reader != null) {
157 					try {
158 						reader.close();
159 					} catch (IOException e) {}
160 				}
161 				if (in != null) {
162 					try {
163 						in.close();
164 					} catch (IOException e) {}
165 				}
166 				if (writer != null) {
167 					try {
168 						writer.close();
169 					} catch (IOException e) {}
170 				}
171 			}
172 			
173 			return success;
174 		}
175 		
176 		public static boolean setDoctypeOnStream(Reader reader, Writer writer, String doctype, String name, Logger logger) throws IOException {
177 			int offset = -1;
178 			int r;
179 			char currentCharacter = ' ';
180 			char nextCharacter = ' ';
181 			char nextNextCharacter = ' ';
182 			char nextNextNextCharacter = ' ';
183 			int isReadingDoctype = 0;
184 			boolean foundDoctype = false;
185 			boolean isReadingPI = false;
186 			boolean isReadingElement = false;
187 			boolean isReadingComment = false;
188 			char isReadingAttribute = ' ';
189 			int skipChars = 0;
190 			
191 			if ((r = reader.read()) != -1) {
192 				nextCharacter = (char) r;
193 				offset++;
194 				
195 				if ((r = reader.read()) != -1) {
196 					nextNextCharacter = (char) r;
197 					offset++;
198 					
199 					if ((r = reader.read()) != -1) {
200 						nextNextNextCharacter = (char) r;
201 						offset++;
202 					}
203 				}
204 			}
205 			
206 			while ((r = reader.read()) != -1) {
207 				offset++;
208 				
209 				// warn after 100K chars, break after 1M chars, we can possibly reduce these if this becomes a bottleneck
210 				if (offset == 100000) {
211 					if (logger != null) {
212 						logger.warn("SetDoctype: More than 100 000 characters in prolog; this is probably not a XML document but will keep looking for DOCTYPE a little while longer: "+name);
213 					}
214 				}
215 				if (offset > 1000000) {
216 					if (logger != null) {
217 						logger.warn("SetDoctype: More than 1 000 000 characters in prolog; this is extremely unlikely to be a XML document; will abort: "+name);
218 					}
219 					writer.close();
220 					writer = null; // clear result contents
221 					break;
222 				}
223 
224 				currentCharacter = nextCharacter;
225 				nextCharacter = nextNextCharacter;
226 				nextNextCharacter = nextNextNextCharacter;
227 				nextNextNextCharacter = (char) r;
228 				
229 				if (skipChars > 0) {
230 					skipChars--;
231 					
232 				} else if (currentCharacter == '<' && nextCharacter == '!' && nextNextCharacter == '-' && nextNextNextCharacter == '-') {
233 					// "<!--"
234 					logger.debug("SetDoctype: \"<!--\"");
235 					isReadingComment = true;
236 					
237 				} else if (currentCharacter == '-' && nextCharacter == '-' && nextNextCharacter == '>' && isReadingComment) {
238 					// "-->" and is reading comment
239 					logger.debug("SetDoctype: \"-->\" and is not reading comment");
240 					isReadingComment = false;
241 					skipChars = 2;
242 					
243 				} else if (currentCharacter == '<' && nextCharacter == '?' && !isReadingComment) {
244 					// "<?" and is not reading comment
245 					logger.debug("SetDoctype: \"<?\" and is not reading comment");
246 					isReadingPI = true;
247 
248 				} else if (currentCharacter == '<' && nextCharacter == '!' && "DdEeAaNn".indexOf(""+nextNextCharacter) != -1 && !isReadingComment) {
249 					// "<!D" or "<!E" etc. and is not reading comment
250 					logger.debug("SetDoctype: \"<!D\" or \"<!E\" etc. and is not reading comment");
251 					isReadingDoctype++;
252 					foundDoctype = true;
253 
254 				} else if (currentCharacter == '?' && nextCharacter == '>' && !isReadingComment && isReadingAttribute == ' ') {
255 					// "?>" and is not reading comment or attribute
256 					logger.debug("SetDoctype: \"?>\" and is not reading comment or attribute");
257 					isReadingPI = false;
258 					skipChars = 1;
259 
260 				} else if (currentCharacter == '>' && !isReadingComment && isReadingAttribute == ' ') {
261 					// ">" and is not reading comment or attribute
262 					logger.debug("SetDoctype: \">\" and is not reading comment or attribute");
263 					
264 					isReadingDoctype--;
265 					
266 					if (isReadingDoctype == 0) {
267 						logger.debug("SetDoctype: inserting doctype");
268 						writer.write(doctype);
269 						writer.write(nextCharacter);
270 						writer.write(nextNextCharacter);
271 						writer.write(nextNextNextCharacter);
272 						
273 						logger.debug("SetDoctype: done reading doctype");
274 						break; // done reading doctype
275 					}
276 
277 				} else if ((currentCharacter == '\'' || currentCharacter == '"') && (isReadingElement || isReadingPI || isReadingDoctype > 0) && (isReadingAttribute == currentCharacter || isReadingAttribute == ' ')) {
278 					if (currentCharacter != isReadingAttribute) {
279 						// found attribute value start
280 						logger.debug("SetDoctype: found attribute value start");
281 						isReadingAttribute = currentCharacter;
282 					} else {
283 						// found attribute value end
284 						logger.debug("SetDoctype: found attribute value end");
285 						isReadingAttribute = ' ';
286 					}
287 				
288 				} else if (currentCharacter == '<' && (nextCharacter != '!' && nextCharacter != '?') && !isReadingComment) {
289 					// found root element
290 					
291 					logger.debug("SetDoctype: found root element, inserting doctype");
292 					writer.write(doctype);
293 					writer.write("\n");
294 					writer.write(currentCharacter);
295 					writer.write(nextCharacter);
296 					writer.write(nextNextCharacter);
297 					writer.write(nextNextNextCharacter);
298 					
299 					logger.debug("SetDoctype: found root element; stop searching for doctype");
300 					break; // found root element; stop searching for doctype
301 				}
302 				
303 				if (!foundDoctype) {
304 					logger.debug("SetDoctype: not found doctype yet, writing: \""+currentCharacter+"\"");
305 					writer.write(currentCharacter);
306 				}
307 			}
308 			
309 			if (offset < 3) {
310 				writer.write(doctype);
311 				writer.write("\n");
312 				if (offset >= 0) {
313 					writer.write(nextCharacter);
314 				}
315 				if (offset >= 1) {
316 					writer.write(nextNextCharacter);
317 				}
318 				if (offset >= 2) {
319 					writer.write(nextNextNextCharacter);
320 				}
321 			}
322 			
323 			// Write remaining characters from reader to writer
324 			logger.debug("SetDoctype: Write remaining characters from reader to writer");
325 			int bufferSize = 1024;
326 			char[] buffer = new char[bufferSize];
327 			int length = 0;
328 			do {
329 				length = reader.read(buffer, 0, bufferSize);
330 				if (length > 0) {
331 					writer.write(buffer, 0, length);
332 				}
333 			} while (length > 0);
334 			
335 			return true;
336 			
337 		}
338 	}
339 
340 }