1   package org.daisy.pipeline.file;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.Reader;
6   import java.io.StringReader;
7   import java.net.URI;
8   import java.net.URISyntaxException;
9   import java.nio.file.FileSystems;
10  import java.nio.file.Path;
11  import java.nio.file.ProviderNotFoundException;
12  import java.util.Optional;
13  
14  import org.daisy.common.file.URLs;
15  
16  /**
17   * A collection of file/document/URI related utility functions.
18   */
19  public final class FileUtils {
20  
21  	private FileUtils() {}
22  
23  	/**
24  	 * Create a <a href="https://www.w3.org/TR/xproc/#cv.result"><code>c:result</code></a> document
25  	 *
26  	 * @param text the value of the text node that the c:result element should contain
27  	 * @return the document as a Reader object
28  	 */
29  	public static Reader cResultDocument(String text) {
30  		StringBuilder sb = new StringBuilder();
31  		sb.append("<c:result xmlns:c=\"http://www.w3.org/ns/xproc-step\">")
32  			.append(text)
33  			.append("</c:result>");
34  		return new StringReader(sb.toString());
35  	}
36  
37  	/* ============= */
38  	/* URI functions */
39  	/* ============= */
40  
41  	/**
42  	 * Convert an absolute file URI to a {@link Path} denoting a file or an entry in a zip file.
43  	 */
44  	public static Path asPath(URI uri) throws IllegalArgumentException, URISyntaxException {
45  		if (!uri.isAbsolute())
46  			throw new IllegalArgumentException("expected absolute URI");
47  		String protocol = uri.getScheme();
48  		if (!"file".equals(protocol))
49  			throw new IllegalArgumentException("expected file URI");
50  		String query = uri.getQuery();
51  		if (query != null && !query.isEmpty())
52  			throw new IllegalArgumentException("expected URI without query");
53  		String fragment = uri.getFragment();
54  		if (fragment != null && !fragment.isEmpty())
55  			throw new IllegalArgumentException("expected URI without fragment");
56  		String path = uri.getPath();
57  		String zipPath = null;
58  		if (path.contains("!/")) {
59  			// it is a path to a ZIP entry
60  			zipPath = path.substring(path.indexOf("!/")+1);
61  			path = path.substring(0, path.indexOf("!/"));
62  		}
63  		File file = new File(new URI(protocol, null, path, null, null));
64  		if (file.exists() && zipPath != null)
65  			try {
66  				return FileSystems.newFileSystem(file.toPath(), null).getPath(zipPath);
67  			} catch (ProviderNotFoundException e) {
68  				throw new RuntimeException(e); // file is not a zip file
69  			} catch (IOException e) {
70  				throw new RuntimeException(e); // should not happen
71  			}
72  		else
73  			return file.toPath();
74  	}
75  
76  	/**
77  	 * Relativize a URI against a base URI.
78  	 *
79  	 * This functions differs from {@link URI#relativize} in that it is also able to find
80  	 * relative paths when the URI does not start with the base URI (in which case the
81  	 * resulting URI will contain ".." segments).
82  	 */
83  	public static URI relativizeURI(URI uri, URI base) {
84  		try {
85  			if (base.isOpaque() || uri.isOpaque()
86  			    || !Optional.ofNullable(base.getScheme()).orElse("").equalsIgnoreCase(Optional.ofNullable(uri.getScheme()).orElse(""))
87  			    || !Optional.ofNullable(base.getAuthority()).equals(Optional.ofNullable(uri.getAuthority())))
88  				return uri;
89  			else {
90  				String up = uri.normalize().getPath();
91  				String bp = base.normalize().getPath();
92  				String relativizedPath;
93  				if (up.startsWith("/")) {
94  					String[] upSegments = up.split("/", -1);
95  					String[] bpSegments = bp.split("/", -1);
96  					int i = bpSegments.length - 1;
97  					int j = 0;
98  					while (i > 0) {
99  						if (bpSegments[j].equals(upSegments[j])) {
100 							i--;
101 							j++; }
102 						else
103 							break; }
104 					relativizedPath = "";
105 					while (i > 0) {
106 						relativizedPath += "../";
107 						i--; }
108 					while (j < upSegments.length) {
109 						relativizedPath += upSegments[j] + "/";
110 						j++; }
111 					relativizedPath = relativizedPath.substring(0, relativizedPath.length() - 1); }
112 				else
113 					relativizedPath = up;
114 				if (relativizedPath.isEmpty())
115 					relativizedPath = "./";
116 				return new URI(null, null, relativizedPath, uri.getQuery(), uri.getFragment()); }
117 		} catch (URISyntaxException e) {
118 			throw new RuntimeException(e);
119 		}
120 	}
121 
122 	public static URI normalizeURI(URI uri) {
123 		return normalizeURI(uri, false);
124 	}
125 
126 	public static URI normalizeURI(URI uri, boolean dropFragment) {
127 		try {
128 			if (uri.isOpaque())
129 				return uri;
130 			if ("jar".equals(uri.getScheme()))
131 				return URLs.asURI("jar:" + normalizeURI(URLs.asURI(uri.toASCIIString().substring(4))).toASCIIString());
132 			uri = uri.normalize();
133 			String scheme = uri.getScheme();
134 			if (scheme != null) scheme = scheme.toLowerCase();
135 			String authority = uri.getAuthority();
136 			if (authority != null) authority = authority.toLowerCase();
137 			if (authority != null && "http".equals(scheme) && authority.endsWith(":80"))
138 				authority = authority.substring(0, authority.length() - 3);
139 			uri = new URI(scheme, authority, uri.getPath(), uri.getQuery(), dropFragment ? null : uri.getFragment());
140 			// fix path
141 			String path = uri.getPath(); {
142 				// add "/" after trailing ".."
143 				path = path.replaceAll("(^|/)\\.\\.$", "$1../");
144 				// remove leading "/.."
145 				path = path.replaceAll("^/(\\.\\./)+", "/");
146 			}
147 			uri = new URI(uri.getScheme(), uri.getAuthority(), path, uri.getQuery(), uri.getFragment());
148 			uri = expand83(uri);
149 			return uri;
150 		} catch (URISyntaxException e) {
151 			throw new RuntimeException(e);
152 		} catch (IOException e) {
153 			throw new RuntimeException(e);
154 		}
155 	}
156 
157 	/**
158 	 * Expand 8.3 encoded path segments.
159 	 *
160 	 * For instance `C:\DOCUME~1\file.xml` will become `C:\Documents and
161 	 * Settings\file.xml`
162 	 */
163 	public static String expand83(String uri) throws URISyntaxException, IOException {
164 		if (uri == null || !uri.startsWith("file:/")) {
165 			return uri;
166 		}
167 		return expand83(URLs.asURI(uri)).toASCIIString();
168 	}
169 
170 	private static URI expand83(URI uri) throws URISyntaxException, IOException {
171 		if (uri == null || !"file".equals(uri.getScheme())) {
172 			return uri;
173 		}
174 		String protocol = "file";
175 		String path = uri.getPath();
176 		String zipPath = null;
177 		if (path.contains("!/")) {
178 			// it is a path to a ZIP entry
179 			zipPath = path.substring(path.indexOf("!/")+1);
180 			path = path.substring(0, path.indexOf("!/"));
181 		}
182 		String query = uri.getQuery();
183 		String fragment = uri.getFragment();
184 		File file = new File(new URI(protocol, null, path, null, null));
185 		URI expandedUri = expand83(file, path.endsWith("/"));
186 		if (expandedUri == null) {
187 			return uri;
188 		} else {
189 			path = expandedUri.getPath();
190 			if (zipPath != null)
191 				path = path + "!" + new URI(null, null, zipPath, null, null).getPath();
192 			return new URI(protocol, null, path, query, fragment);
193 		}
194 	}
195 
196 	public static URI expand83(File file) throws URISyntaxException, IOException {
197 		return expand83(file, false);
198 	}
199 
200 	private static URI expand83(File file, boolean isDir) throws URISyntaxException, IOException {
201 		if (file.exists()) {
202 			return URLs.asURI(file.getCanonicalFile());
203 		} else {
204 			// if the file does not exist a parent directory may exist which can be canonicalized
205 			String relPath = file.getName();
206 			if (isDir)
207 				relPath += "/";
208 			File dir = file.getParentFile();
209 			while (dir != null) {
210 				if (dir.exists())
211 					return URLs.resolve(URLs.asURI(dir.getCanonicalFile()), new URI(null, null, relPath, null, null));
212 				relPath = dir.getName() + "/" + relPath;
213 				dir = dir.getParentFile();
214 			}
215 			return URLs.asURI(file);
216 		}
217 	}
218 }