View Javadoc

1   /*
2    * #%L
3    * JavaHg
4    * %%
5    * Copyright (C) 2011 aragost Trifork ag
6    * %%
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   * 
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   * 
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   * #L%
25   */
26  package com.aragost.javahg.internals;
27  
28  import java.io.BufferedOutputStream;
29  import java.io.File;
30  import java.io.FileOutputStream;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.InputStreamReader;
34  import java.io.OutputStream;
35  import java.lang.reflect.Array;
36  import java.lang.reflect.InvocationTargetException;
37  import java.nio.ByteBuffer;
38  import java.nio.CharBuffer;
39  import java.nio.charset.CharacterCodingException;
40  import java.nio.charset.Charset;
41  import java.nio.charset.CharsetDecoder;
42  import java.util.Collection;
43  import java.util.Map;
44  import java.util.Random;
45  import java.util.concurrent.ExecutionException;
46  import java.util.regex.Pattern;
47  
48  import com.aragost.javahg.log.Logger;
49  import com.aragost.javahg.log.LoggerFactory;
50  import com.google.common.io.ByteStreams;
51  import com.google.common.io.CharStreams;
52  
53  /**
54   * Miscellaneous utils methods for JavaHg.
55   */
56  public class Utils {
57  
58      private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
59  
60      /**
61       * Flag indicating if we are on a Windows box
62       */
63      private static final boolean IS_WINDOWS;
64  
65      /**
66       * Temp dir where resources are copied from the jar (classpath) to
67       * an actual file
68       */
69      private static final File RESOURCES_TEMP_DIR;
70      static {
71          String osName = System.getProperty("os.name");
72          IS_WINDOWS = osName.startsWith("Windows");
73          File baseDirectory = new File(System.getProperty("user.home"), ".javahg" + File.separator + "tmp");
74          RESOURCES_TEMP_DIR = createTempDirectory(baseDirectory);
75          Runtime.getRuntime().addShutdownHook(new Thread() {
76              @Override
77              public void run() {
78                  try {
79                      Utils.deleteRecursive(RESOURCES_TEMP_DIR);
80                      LOG.info("Deleted temp resource dir " + RESOURCES_TEMP_DIR);
81                  } catch (IOException e) {
82                      System.err.println("Could not delete temp resource dir " + RESOURCES_TEMP_DIR);
83                      e.printStackTrace();
84                  }
85              }
86          });
87      }
88  
89  	private static final Pattern HTTP_PATTERN = Pattern.compile("[hH][tT][tT][pP]:.*[@]");
90  	private static final Pattern HTTPS_PATTERN = Pattern.compile("[hH][tT][tT][pP][sS]:.*[@]");
91  	private static final Pattern SSH_PATTERN = Pattern.compile("[sS][sS][hH]:.*[@]");
92  	private static final Pattern SVN_PATTERN = Pattern.compile("[sS][vV][nN]:.*[@]");
93  
94      /**
95       * 
96       * @return true if the OS is Windows, otherwise false
97       */
98      public static boolean isWindows() {
99          return IS_WINDOWS;
100     }
101 
102     public static Charset getUtf8Charset() {
103         return Charset.forName("UTF-8");
104     }
105 
106     /**
107      * Read four bytes from the stream, and return the integer
108      * represented by these bytes in big endian. If EOF is reached
109      * then -1 is returned.
110      * 
111      * @param stream
112      *            the input stream
113      * @return the decoded integer.
114      * @throws IOException
115      */
116     public static int readBigEndian(InputStream stream) throws IOException {
117         byte b0 = (byte) stream.read();
118         byte b1 = (byte) stream.read();
119         byte b2 = (byte) stream.read();
120         int b3 = stream.read();
121         if (b3 == -1) {
122             return -1;
123         }
124         return (b0 << 24) + ((b1 & 0xFF) << 16) + ((b2 & 0xFF) << 8) + ((byte) b3 & 0xFF);
125     }
126 
127     /**
128      * Write 4 bytes representing the specified integer in big-endian.
129      * 
130      * @param n
131      *            the integer
132      * @param stream
133      *            the output stream
134      * @throws IOException
135      */
136     public static void writeBigEndian(int n, OutputStream stream) throws IOException {
137         byte[] bytes = new byte[] { (byte) (n >>> 24), (byte) (n >>> 16), (byte) (n >>> 8), (byte) n };
138         stream.write(bytes);
139     }
140 
141     /**
142      * Read and discard everything from the specified InputStream
143      * 
144      * @param stream
145      * @throws IOException
146      */
147     public static void consumeAll(InputStream stream) throws IOException {
148         while (stream.read(BUF) != -1)
149             ;
150     }
151 
152     private static final byte[] BUF = new byte[256];
153 
154     /**
155      * Read everything from the stream and return it as a String
156      * 
157      * @param in
158      *            the input stream
159      * @param decoder
160      *            the {@link CharsetDecoder} encapsulating the policy
161      *            of handling errors while decoding.
162      * @return the decoded String.
163      */
164     public static String readStream(InputStream in, CharsetDecoder decoder) {
165         try {
166             return CharStreams.toString(new InputStreamReader(in, decoder));
167         } catch (IOException e) {
168             throw new RuntimeIOException(e);
169         }
170     }
171 
172     /**
173      * Decode the bytes with the decoder
174      * 
175      * @param bytes
176      * @param decoder
177      * @return the decoded bytes
178      */
179     public static String decodeBytes(byte[] bytes, CharsetDecoder decoder) {
180         ByteBuffer wrap = ByteBuffer.wrap(bytes);
181         CharBuffer chars;
182         try {
183             chars = decoder.decode(wrap);
184         } catch (CharacterCodingException e) {
185             throw asRuntime(e);
186         }
187         return new String(chars.array(), chars.arrayOffset(), chars.limit());
188     }
189 
190     /**
191      * Insert an element as the first to an array. The input array is
192      * not changed, instead a copy is returned.
193      * 
194      * @param <T>
195      * @param first
196      *            element to insert at position 0 in the result.
197      * @param rest
198      *            elements to insert at position 1 to
199      *            {@code rest.length}.
200      * @return the unshifted array of length {@code 1 + rest.length}.
201      */
202     public static <T> T[] arrayUnshift(T first, T[] rest) {
203         @SuppressWarnings("unchecked")
204         T[] result = (T[]) Array.newInstance(first.getClass(), 1 + rest.length);
205         result[0] = first;
206         System.arraycopy(rest, 0, result, 1, rest.length);
207         return result;
208     }
209 
210     /**
211      * Concatenate two arrays. The input arrays are copied into the
212      * output array.
213      * 
214      * @param <T>
215      * @param a
216      * @param b
217      * @return the concatenated array of length
218      *         {@code a.length + b.length}.
219      */
220     public static <T> T[] arrayConcat(T[] a, T[] b) {
221         @SuppressWarnings("unchecked")
222         T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length);
223         System.arraycopy(a, 0, result, 0, a.length);
224         System.arraycopy(b, 0, result, a.length, b.length);
225         return result;
226     }
227 
228     /**
229      * Convert an array of Files to an array of Strings.
230      * <p>
231      * Each file is converted with the getPath() method.
232      * 
233      * @param files
234      * @return array of file names as Strings
235      */
236     public static String[] fileArray2StringArray(File[] files) {
237         String[] result = new String[files.length];
238         for (int i = 0; i < files.length; i++) {
239             File file = files[i];
240             result[i] = file.getPath();
241         }
242         return result;
243     }
244 
245     /**
246      * 
247      * @param baseDir
248      * @param strings
249      * @return array of File objects
250      */
251     public static File[] stringArray2FileArray(File baseDir, String[] strings) {
252         File[] result = new File[strings.length];
253         for (int i = 0; i < strings.length; i++) {
254             String s = strings[i];
255             File file = new File(s);
256             if (!file.isAbsolute()) {
257                 file = new File(baseDir, s);
258             }
259             result[i] = Utils.resolveSymlinks(file);
260         }
261         return result;
262     }
263 
264     /**
265      * Return a new File object where symlinks are resolved.
266      * 
267      * @param file
268      * @return the canonical file name
269      */
270     public static File resolveSymlinks(File file) {
271         // On windows calling File.getCanonicalFile will result in
272         // abc~ parts will be expanded. We don't want that.
273         if (IS_WINDOWS) {
274             return file;
275         } else {
276             try {
277                 return file.getCanonicalFile();
278             } catch (IOException e) {
279                 throw new RuntimeIOException(e);
280             }
281         }
282     }
283 
284     /**
285      * Return the single element in the specified Collection.
286      * <p>
287      * If the collection is empty <code>null</null> is return
288      * 
289      * @param coll
290      * @return null or the singleton element in the collection
291      * @throws IllegalArgumentException
292      *             if the collection has more than one element
293      */
294     public static <T> T single(Collection<T> coll) {
295         switch (coll.size()) {
296         case 0:
297             return null;
298         case 1:
299             return coll.iterator().next();
300         default:
301             throw new IllegalArgumentException("Collection has more than one element");
302         }
303     }
304 
305     /**
306      * Create a temporary file.
307      * <p>
308      * This method is identical to
309      * {@link java.io.File#createTempFile(String, String)} except it
310      * throws a {@link RuntimeIOException}.
311      * 
312      * @param suffix
313      * @return the temporary file
314      */
315     public static File createTempFile(String suffix) {
316         try {
317             return File.createTempFile("javahg-", suffix);
318         } catch (IOException e) {
319             throw new RuntimeIOException(e);
320         }
321     }
322 
323     /**
324      * Determines whether the specified file is a Symbolic Link rather
325      * than an actual file.
326      * <p>
327      * Will not return true if there is a Symbolic Link anywhere in
328      * the path, only if the specific file is.
329      * 
330      * Note: The implementation is adapted from the similar method in
331      * Apache Commons IO
332      * 
333      * @param file
334      *            the file to check
335      * @return true if the file is a Symbolic Link
336      */
337     public static boolean isSymlink(File file) {
338         if (IS_WINDOWS) {
339             return false;
340         }
341         try {
342             File fileInCanonicalDir = null;
343             if (file.getParent() == null) {
344                 fileInCanonicalDir = file;
345             } else {
346                 File canonicalDir = file.getParentFile().getCanonicalFile();
347                 fileInCanonicalDir = new File(canonicalDir, file.getName());
348             }
349 
350             return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
351         } catch (IOException e) {
352             throw new RuntimeIOException(e);
353         }
354     }
355 
356     /**
357      * Convert the specified exception into a RuntimeException
358      * 
359      * @param e
360      * @return the RuntimeException
361      */
362     public static RuntimeException asRuntime(Throwable e) {
363         if (e instanceof RuntimeException) {
364             return (RuntimeException) e;
365         }
366         if (e instanceof ExecutionException) {
367             return asRuntime(e.getCause());
368         }
369         if (e instanceof InvocationTargetException) {
370             return asRuntime(e.getCause());
371         }
372         if (e instanceof IOException) {
373             return new RuntimeIOException((IOException) e);
374         }
375         return new RuntimeException(e);
376     }
377 
378     /**
379      * Delete a directory in the system temporary directory
380      * (java.io.tmpdir).
381      * 
382      * @throws IOException
383      */
384     public static void deleteTempDir(File file) throws IOException {
385         file = file.getCanonicalFile();
386         String javaTmpDir = new File(System.getProperty("java.io.tmpdir")).getCanonicalPath();
387         if (file.getPath().startsWith(javaTmpDir)) {
388             deleteRecursive(file);
389         } else {
390             throw new IllegalArgumentException("Can only delete files in " + javaTmpDir);
391         }
392     }
393 
394     private static void deleteRecursive(File file) throws IOException {
395         if (file.isDirectory()) {
396             File[] files = file.listFiles();
397             for (File f : files) {
398                 deleteRecursive(f);
399             }
400         }
401 
402         // Windows often fails to delete on first attempt, but after a
403         // short
404         // break the file can be deleted. Wait up to 5 sec.
405         // I believe that it is because the cmdserver process (which
406         // has been
407         // terminated before calling this method)
408         // still has a lock on the dir. So we wait for it to actually
409         // finish and
410         // release lock
411         for (int i = 20; !file.delete(); i--) {
412             if (i == 0) {
413                 throw new IOException("Failed to delete: " + file);
414             }
415             try {
416                 System.err.println("Sleep a bit and try again to delete " + file + "[" + i + "/20]");
417                 Thread.sleep(250);
418             } catch (InterruptedException e) {
419             }
420         }
421     }
422     
423     /**
424      * Creates a new temporary directory as child of the given parent.
425      * 
426      * @param parent parent directory
427      * @return temporary directory
428      */
429     private static File createTempDirectory( File parent ) {
430         String name = System.currentTimeMillis() + "-";
431         for (int i=0; i<100; i++) {
432             File directory = new File(parent, name + i);
433             if ( ! directory.exists() && directory.mkdirs() ){
434                 return directory;
435             }
436         }
437         throw new IllegalStateException("could not create a temp directory at " + parent);
438     }
439 
440     /**
441      * Return a File that is created from a resource name found on the
442      * classpath. The resource is written into the File the first time
443      * it's needed.
444      * 
445      * @param resourceName
446      * @return the File
447      * @throws IOException
448      */
449     public static File resourceAsFile(String resourceName) {
450         return resourceAsFile(resourceName, null);
451     }
452 
453     /**
454      * Return a File that is created from a resource name found on the
455      * classpath. The resource is written into the File the first time
456      * it's needed.
457      * <p>
458      * Patterns of the form %{name} will be replace with the
459      * corresponding value from the replacement map.
460      * 
461      * @param resourceName
462      * @parem replacements if null no replacements will be performed
463      * @return the File
464      * @throws IOException
465      */
466     public static File resourceAsFile(String resourceName, Map<String, byte[]> replacements) {
467         File file = new File(RESOURCES_TEMP_DIR, resourceName);
468         if (!file.exists()) {
469             file.getParentFile().mkdirs();
470             InputStream stream = Utils.class.getResourceAsStream(resourceName);
471             try {
472                 OutputStream fileOutput = new BufferedOutputStream(new FileOutputStream(file));
473                 OutputStream output = fileOutput;
474                 if (replacements != null) {
475                     output = new PatternReplacingOutputStream(fileOutput, replacements);
476                 }
477                 ByteStreams.copy(stream, output);
478                 output.close();
479             } catch (IOException e) {
480                 throw new RuntimeIOException(e);
481             }
482             LOG.info("Copy resource {} to {}", resourceName, file.getAbsolutePath());
483         }
484         return file;
485     }
486 
487     /**
488      * Generate some random bytes that is printable Ascii characters.
489      * <p>
490      * They will be used in a Mercurial style file. To make it easy to
491      * use the bytes in the style file the characters '"' and '\' will
492      * not be part of the byte array.
493      * 
494      * @return
495      */
496     public static byte[] randomBytes() {
497         byte[] bytes = new byte[20];
498         new Random().nextBytes(bytes);
499         // Make the bytes printable by modulo 64 and add to '"' and
500         // use 126 for '\\'
501         assert '"' + 64 < 126;
502         for (int i = 0; i < bytes.length; i++) {
503             int b = '"' + 1 + (bytes[i] & 63);
504             if (b == '\\') {
505                 b = 126;
506             }
507             bytes[i] = (byte) b;
508         }
509         LOG.info("Random bytes generated: {}", new String(bytes));
510         return bytes;
511     }
512 
513 	public static String obfuscateLoginData(String line) {
514 		String myLine = (line == null ? "" : line);
515 		myLine = HTTP_PATTERN.matcher(myLine).replaceAll("http://***@");
516 		if (myLine.equals(line)) {
517 			myLine = HTTPS_PATTERN.matcher(line).replaceAll("https://***@");
518 		}
519 		if (myLine.equals(line)) {
520 			myLine = SSH_PATTERN.matcher(line).replaceAll("ssh://***@");
521 		}
522 		if (myLine.equals(line)) {
523 			myLine = SVN_PATTERN.matcher(line).replaceAll("svn://***@");
524 		}
525 		return myLine;
526 	}
527 }