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
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;
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
239 logger.debug("SetDoctype: \"-->\" and is not reading comment");
240 isReadingComment = false;
241 skipChars = 2;
242
243 } else if (currentCharacter == '<' && nextCharacter == '?' && !isReadingComment) {
244
245 logger.debug("SetDoctype: \"<?\" and is not reading comment");
246 isReadingPI = true;
247
248 } else if (currentCharacter == '<' && nextCharacter == '!' && "DdEeAaNn".indexOf(""+nextNextCharacter) != -1 && !isReadingComment) {
249
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
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
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;
275 }
276
277 } else if ((currentCharacter == '\'' || currentCharacter == '"') && (isReadingElement || isReadingPI || isReadingDoctype > 0) && (isReadingAttribute == currentCharacter || isReadingAttribute == ' ')) {
278 if (currentCharacter != isReadingAttribute) {
279
280 logger.debug("SetDoctype: found attribute value start");
281 isReadingAttribute = currentCharacter;
282 } else {
283
284 logger.debug("SetDoctype: found attribute value end");
285 isReadingAttribute = ' ';
286 }
287
288 } else if (currentCharacter == '<' && (nextCharacter != '!' && nextCharacter != '?') && !isReadingComment) {
289
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;
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
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 }