1   package org.daisy.pipeline.file.calabash.impl;
2   
3   import java.io.InputStream;
4   import java.io.IOException;
5   import java.net.URI;
6   import java.net.URISyntaxException;
7   import java.nio.file.Path;
8   import java.nio.file.Files;
9   import java.util.Map;
10  
11  import com.xmlcalabash.core.XProcConstants;
12  import com.xmlcalabash.core.XProcRuntime;
13  import com.xmlcalabash.io.WritablePipe;
14  import com.xmlcalabash.library.DefaultStep;
15  import com.xmlcalabash.model.RuntimeValue;
16  import com.xmlcalabash.runtime.XAtomicStep;
17  import com.xmlcalabash.util.Base64;
18  import com.xmlcalabash.util.TreeWriter;
19  
20  import net.sf.saxon.s9api.QName;
21  import net.sf.saxon.s9api.SaxonApiException;
22  
23  import org.daisy.common.xproc.calabash.XProcStep;
24  import org.daisy.common.xproc.calabash.XProcStepProvider;
25  import org.daisy.common.xproc.XProcMonitor;
26  import org.daisy.pipeline.file.FileUtils;
27  
28  import org.osgi.service.component.annotations.Component;
29  
30  @Component(
31  	name = "pxi:file-peek",
32  	service = { XProcStepProvider.class },
33  	property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}file-peek" }
34  )
35  public class PeekProvider implements XProcStepProvider {
36  
37  	@Override
38  	public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
39  		return new Peek(runtime, step);
40  	}
41  
42  	/**
43  	 * Returns a base64-encoded string, skipping the first `offset` bytes, up to a number of `length` bytes, from the file `href`.
44  	 */
45  	public static class Peek extends DefaultStep implements XProcStep {
46  		private static final QName _href = new QName("href");
47  		private static final QName _offset = new QName("offset");
48  		private static final QName _length = new QName("length");
49  
50  		private WritablePipe result = null;
51  
52  		public Peek(XProcRuntime runtime, XAtomicStep step) {
53  			super(runtime,step);
54  		}
55  
56  		public void setOutput(String port, WritablePipe pipe) {
57  			result = pipe;
58  		}
59  
60  		public void reset() {
61  			result.resetWriter();
62  		}
63  
64  		public void run() throws SaxonApiException {
65  			super.run();
66  
67  			int offset = getOption(_offset, 0);
68  			int length = getOption(_length, 0);
69  
70  			RuntimeValue href = getOption(_href);
71  			URI sourceUri = href.getBaseURI().resolve(href.getString());
72  			Path file; {
73  				try {
74  					file = FileUtils.asPath(sourceUri);
75  				} catch (IllegalArgumentException|URISyntaxException e) {
76  					throw XProcStep.raiseError(new IllegalArgumentException("Illegal value for href option: " + href.getString(), e), step);
77  				}
78  			}
79  
80  			if (Files.isDirectory(file)) {
81  				throw XProcStep.raiseError(new IllegalArgumentException("Cannot peek into file: file is a directory: " + file), step);
82  			}
83  
84  			byte[] resultBytes = null;
85  			try {
86  				resultBytes = Peek.read(file, offset, length);
87  			} catch (IOException | IndexOutOfBoundsException e) {
88  				String fileSize; {
89  					try {
90  						fileSize = "" + Files.size(file);
91  					} catch (IOException ee) {
92  						fileSize = "?";
93  					}
94  				}
95  				logger.error("px:file-peek failed to read from "+file+" (offset: "+offset+", length: "+length+", filesize: "+fileSize+")", e);
96  				e.printStackTrace();
97  			}
98  
99  			TreeWriter tree = new TreeWriter(runtime);
100 			tree.startDocument(step.getNode().getBaseURI());
101 			tree.addStartElement(XProcConstants.c_result);
102 			tree.addAttribute(new QName("content-type"), "binary/octet-stream");
103 			tree.addAttribute(new QName("encoding"), "base64");
104 			tree.startContent();
105 			
106 			if (resultBytes == null) {
107 				String fileSize; {
108 					try {
109 						fileSize = "" + Files.size(file);
110 					} catch (IOException ee) { // should not happen
111 						fileSize = "?";
112 					}
113 				}
114 				tree.addAttribute(new QName("error"), "px:file-peek failed to read from "+file+" (offset: "+offset+", length: "+length+", filesize: "+fileSize+")");
115 			} else {
116 				tree.addText(Peek.encodeBase64(resultBytes));
117 			}
118 
119 			tree.addEndElement();
120 			tree.endDocument();
121 
122 			result.write(tree.getResult());
123 		}
124 		
125 		public static byte[] read(Path file, int offset, int length) throws IOException, IndexOutOfBoundsException {
126 			long fileSize = Files.size(file);
127 			if (offset > fileSize) {
128 				offset = 0;
129 				length = 0;
130 			}
131 			if (length + offset > fileSize) {
132 				length = (int) (fileSize - offset);
133 			}
134 
135 			InputStream fis = null;
136 			byte[] resultBytes = new byte[length];
137 			if (length > 0) {
138 				try {
139 					if (Files.exists(file) && Files.isReadable(file)) {
140 						fis = Files.newInputStream(file);
141 						fis.skip(offset);
142 						fis.read(resultBytes, 0, length);
143 					}
144 
145 				} finally {
146 					try {
147 						fis.close();
148 					} catch (IOException e) { // Ignore any exceptions here
149 					}
150 				}
151 			}
152 			
153 			return resultBytes;
154 		}
155 
156 		public static String encodeBase64(byte[] bytes) {
157 			return Base64.encodeBytes(bytes);
158 		}
159 	}
160 
161 }