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 com.xmlcalabash.core.XProcConstants;
19  import com.xmlcalabash.core.XProcException;
20  import com.xmlcalabash.core.XProcRuntime;
21  import com.xmlcalabash.io.WritablePipe;
22  import com.xmlcalabash.library.DefaultStep;
23  import com.xmlcalabash.model.RuntimeValue;
24  import com.xmlcalabash.runtime.XAtomicStep;
25  import com.xmlcalabash.util.TreeWriter;
26  
27  import net.sf.saxon.s9api.QName;
28  import net.sf.saxon.s9api.SaxonApiException;
29  
30  import org.daisy.common.xproc.calabash.XProcStep;
31  import org.daisy.common.xproc.calabash.XProcStepProvider;
32  import org.daisy.common.xproc.XProcMonitor;
33  
34  import org.slf4j.Logger;
35  
36  import org.osgi.service.component.annotations.Component;
37  
38  @Component(
39  	name = "px:set-xml-declaration",
40  	service = { XProcStepProvider.class },
41  	property = { "type:String={http://www.daisy.org/ns/pipeline/xproc}set-xml-declaration" }
42  )
43  public class SetXmlDeclarationProvider implements XProcStepProvider {
44  
45  	@Override
46  	public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
47  		return new SetXmlDeclaration(runtime, step);
48  	}
49  
50  	public static class SetXmlDeclaration extends DefaultStep implements XProcStep {
51  		private static final QName _href = new QName("href");
52  		private static final QName _xmlDeclaration = new QName("xml-declaration");
53  		private static final QName _encoding = new QName("encoding");
54  
55  		private WritablePipe result = null;
56  
57  		public SetXmlDeclaration(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 xmlDeclaration = getOption(_xmlDeclaration, "");
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 xml declaration of file: file is a directory: " + file.getAbsolutePath());
81  			}
82  			
83  			boolean ok = false;
84  			ok = SetXmlDeclaration.setXmlDeclaration(file, encoding, xmlDeclaration, 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("SetXmlDeclaration: successfully set the XML declaration");
94  				tree.addText(file.toURI().toString());
95  			} else {
96  				String errorMessage = "px:set-xml-declaration failed to read from "+file+" (xml declaration: "+xmlDeclaration+", filesize: "+(file==null?'?':file.length())+")";
97  				logger.warn("SetXmlDeclaration: "+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 setXmlDeclaration(File file, String encoding, String xmlDeclaration, Logger logger) {
108 			
109 			logger.debug("SetXmlDeclaration: file=["+file+"]");
110 			logger.debug("SetXmlDeclaration: encoding=["+encoding+"]");
111 			logger.debug("SetXmlDeclaration: xmlDeclaration=["+xmlDeclaration+"]");
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("setXmlDeclaration", "");
130 				writer = new OutputStreamWriter(new FileOutputStream(tempFile), encoding);
131 				
132 				success = setXmlDeclarationOnStream(reader, writer, xmlDeclaration, file.toString(), logger);
133 				try {
134 					writer.close();
135 				} catch (IOException e) {
136 					logger.error("SetXmlDeclaration: unable to close OutputStreamWriter", e);
137 				}
138 				
139 				if (success) {
140 				    Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
141 				}
142 				
143 			} catch (IOException e) {
144 				if (logger != null) {
145 					logger.error("SetXmlDeclaration: px:set-xml-declaration failed to read from "+file, e);
146 				}
147 				e.printStackTrace();
148 
149 			} finally {
150 				if (reader != null) {
151 					try {
152 						reader.close();
153 					} catch (IOException e) {}
154 				}
155 				if (in != null) {
156 					try {
157 						in.close();
158 					} catch (IOException e) {}
159 				}
160 				if (writer != null) {
161 					try {
162 						writer.close();
163 					} catch (IOException e) {}
164 				}
165 			}
166 			
167 			return success;
168 		}
169 		
170 		public static boolean setXmlDeclarationOnStream(Reader reader, Writer writer, String xmlDeclaration, String name, Logger logger) throws IOException {
171 			int offset = -1;
172 			int r;
173 			char currentCharacter = ' ';
174 			char nextCharacter = ' ';
175 			
176 			if ((r = reader.read()) != -1) {
177 				nextCharacter = (char) r;
178 				offset++;
179 			}
180 			
181 			while ((r = reader.read()) != -1) {
182 				offset++;
183 				
184 				currentCharacter = nextCharacter;
185 				nextCharacter = (char) r;
186 				
187 				if (offset == 1 && !(currentCharacter == '<' && nextCharacter == '?')) {
188 					// no existing xml declaration
189 					logger.debug("SetXmlDeclaration: no existing xml declaration");
190 					
191 					if (!"".equals(xmlDeclaration)) {
192 						writer.write(xmlDeclaration);
193 						writer.write("\n");
194 					}
195 					writer.write(currentCharacter);
196 					writer.write(nextCharacter);
197 					break;
198 				}
199 				
200 				if (offset > 1 && (currentCharacter == '?' && nextCharacter == '>')) {
201 					// found end of existing xml declaration
202 					logger.debug("SetXmlDeclaration: found end of existing xml declaration");
203 					
204 					if ("".equals(xmlDeclaration) && (r = reader.read()) != -1) {
205 						offset++;
206 						currentCharacter = nextCharacter;
207 						nextCharacter = (char) r;
208 						
209 						if (nextCharacter != '\n') {
210 							writer.write(nextCharacter);
211 						}
212 						
213 					} else {
214 						writer.write(xmlDeclaration);
215 					}
216 					
217 					break;
218 				}
219 			}
220 			
221 			if (offset < 1) {
222 				writer.write(xmlDeclaration);
223 				writer.write("\n");
224 				if (offset >= 0) {
225 					writer.write(nextCharacter);
226 				}
227 			}
228 			
229 			// Write remaining characters from reader to writer
230 			logger.debug("SetXmlDeclaration: Write remaining characters from reader to writer");
231 			int bufferSize = 1024;
232 			char[] buffer = new char[bufferSize];
233 			int length = 0;
234 			do {
235 				length = reader.read(buffer, 0, bufferSize);
236 				if (length > 0) {
237 					writer.write(buffer, 0, length);
238 				}
239 			} while (length > 0);
240 			
241 			return true;
242 			
243 		}
244 	}
245 
246 }