PageRenderTime 69ms CodeModel.GetById 3ms app.highlight 53ms RepoModel.GetById 1ms app.codeStats 1ms

/okapi/core/src/main/java/net/sf/okapi/common/Util.java

https://bitbucket.org/IevgenTkachuk/okapi
Java | 1900 lines | 1391 code | 72 blank | 437 comment | 178 complexity | fdd81e050829d2b546bc08b0bd1cd20b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*===========================================================================
   2  Copyright (C) 2008-2017 by the Okapi Framework contributors
   3-----------------------------------------------------------------------------
   4  Licensed under the Apache License, Version 2.0 (the "License");
   5  you may not use this file except in compliance with the License.
   6  You may obtain a copy of the License at
   7
   8  http://www.apache.org/licenses/LICENSE-2.0
   9
  10  Unless required by applicable law or agreed to in writing, software
  11  distributed under the License is distributed on an "AS IS" BASIS,
  12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13  See the License for the specific language governing permissions and
  14  limitations under the License.
  15============================================================================*/
  16
  17package net.sf.okapi.common;
  18
  19import java.io.BufferedOutputStream;
  20import java.io.File;
  21import java.io.FileNotFoundException;
  22import java.io.FileOutputStream;
  23import java.io.FilenameFilter;
  24import java.io.IOException;
  25import java.io.OutputStreamWriter;
  26import java.io.PrintWriter;
  27import java.io.PushbackReader;
  28import java.io.Reader;
  29import java.io.UnsupportedEncodingException;
  30import java.io.Writer;
  31import java.net.MalformedURLException;
  32import java.net.URI;
  33import java.net.URISyntaxException;
  34import java.net.URL;
  35import java.net.URLDecoder;
  36import java.net.URLEncoder;
  37import java.nio.ByteBuffer;
  38import java.nio.CharBuffer;
  39import java.nio.charset.CharacterCodingException;
  40import java.nio.charset.Charset;
  41import java.nio.charset.CharsetEncoder;
  42import java.util.Comparator;
  43import java.util.List;
  44import java.util.Locale;
  45import java.util.Map;
  46import java.util.Map.Entry;
  47import java.util.regex.Matcher;
  48import java.util.regex.Pattern;
  49
  50import javax.xml.xpath.XPathFactory;
  51
  52import net.sf.okapi.common.exceptions.OkapiException;
  53import net.sf.okapi.common.exceptions.OkapiIOException;
  54import net.sf.okapi.common.exceptions.OkapiUnsupportedEncodingException;
  55
  56import org.w3c.dom.Node;
  57
  58/**
  59 * Collection of various all-purpose helper functions.
  60 */
  61public final class Util {
  62
  63	/**
  64	 * Name of the root directory variable. 
  65	 */
  66	public static final String ROOT_DIRECTORY_VAR = "${rootDir}";
  67	
  68	/**
  69	 * Name of the input root directory variable.
  70	 */
  71	public static final String INPUT_ROOT_DIRECTORY_VAR = "${inputRootDir}";
  72
  73	/**
  74	 * Enumeration of supported OSes	 	 
  75	 */
  76	public static enum SUPPORTED_OS {
  77		WINDOWS,
  78		MAC,
  79		LINUX		
  80	}
  81
  82	/**
  83	 * Line-break string for DOS/Windows.
  84	 */
  85	public static final String LINEBREAK_DOS = "\r\n";
  86	/**
  87	 * Line-break string for Unix/Linux
  88	 */
  89	public static final String LINEBREAK_UNIX = "\n";
  90	/**
  91	 * Line-break string for Macintosh
  92	 */
  93	public static final String LINEBREAK_MAC = "\r";
  94
  95	/**
  96	 * Default RTF style for starting an external code.
  97	 */
  98	public static final String RTF_STARTCODE = "{\\cs5\\f1\\cf15\\lang1024 ";
  99	/**
 100	 * Default RTF style for ending an external code.
 101	 */
 102	public static final String RTF_ENDCODE = "}";
 103	/**
 104	 * Default RTF style for starting an internal code.
 105	 */
 106	public static final String RTF_STARTINLINE = "{\\cs6\\f1\\cf6\\lang1024 ";
 107	/**
 108	 * Default RTF style for ending an internal code.
 109	 */
 110	public static final String RTF_ENDINLINE = "}";
 111	/**
 112	 * Default RTF style for starting an in-between source/target marker.
 113	 */
 114	public static final String RTF_STARTMARKER = "{\\cs15\\v\\cf12\\sub\\f2 \\{0>}{\\v\\f1 ";
 115	/**
 116	 * Default RTF style for the first half of a middle part of an in-between source/target marker.
 117	 */
 118	public static final String RTF_MIDMARKER1 = "}{\\cs15\\v\\cf12\\sub\\f2 <\\}";
 119	/**
 120	 * Default RTF style for the second half of a middle part of an in-between source/target marker.
 121	 */
 122	public static final String RTF_MIDMARKER2 = "\\{>}";
 123	/**
 124	 * Default RTF style for ending an in-between source/target marker.
 125	 */
 126	public static final String RTF_ENDMARKER = "{\\cs15\\v\\cf12\\sub\\f2 <0\\}}";
 127
 128	private static final String NEWLINES_REGEX = "\r(\n)?";
 129	private static final Pattern NEWLINES_REGEX_PATTERN = Pattern.compile(NEWLINES_REGEX);
 130	/**
 131	 * Any variable of the type ${varname}.
 132	 */
 133	private static final String VARIABLE_REGEX = "\\$\\{([^\\}]+)\\}";
 134	private static final Pattern VARIABLE_REGEX_PATTERN = Pattern.compile(VARIABLE_REGEX);
 135	/**
 136	 * Shared flag indicating a translation that was generated using machine translation.
 137	 */
 138	public static final String MTFLAG = "MT!";
 139	
 140
 141	// Used by openURL()
 142	private static final String[] browsers = { "firefox", "opera", "konqueror", "epiphany",
 143		"seamonkey", "galeon", "kazehakase", "mozilla", "netscape" };
 144
 145	/**
 146	 * Converts all \r\n and \r to linefeed (\n)
 147	 * @param text 
 148	 *      the text to convert
 149	 * @return converted string
 150	 */
 151	static public String normalizeNewlines(String text) {
 152		return NEWLINES_REGEX_PATTERN.matcher(text).replaceAll("\n");
 153	}
 154
 155	/**
 156	 * Removes from the start of a string any of the specified characters.
 157	 * 
 158	 * @param text
 159	 *            string to trim.
 160	 * @param chars
 161	 *            list of the characters to trim.
 162	 * @return The trimmed string.
 163	 */
 164	static public String trimStart (String text,
 165		String chars)
 166	{
 167		if ( text == null ) return text;
 168		int n = 0;
 169		while ( n < text.length() ) {
 170			if ( chars.indexOf(text.charAt(n)) == -1 ) {
 171				break;
 172			}
 173			n++;
 174		}
 175		if ( n >= text.length() ) return "";
 176		if ( n > 0 ) return text.substring(n);
 177		return text;
 178	}
 179
 180	/**
 181	 * Removes from the end of a string any of the specified characters.
 182	 * @param text
 183	 *            string to trim.
 184	 * @param chars
 185	 *            list of the characters to trim.
 186	 * @return the trimmed string.
 187	 */
 188	static public String trimEnd (String text,
 189		String chars)
 190	{
 191		if ( text == null ) return text;
 192		int n = text.length() - 1;
 193		while ( n >= 0 ) {
 194			if (chars.indexOf(text.charAt(n)) == -1)
 195				break;
 196			n--;
 197		}
 198		if ( n < 0 ) return "";
 199		if ( n > 0 ) return text.substring(0, n + 1);
 200		return text;
 201	}
 202
 203	/**
 204	 * Gets the directory name of a full path.
 205	 * 
 206	 * @param path
 207	 *         full path from where to extract the directory name. The path
 208	 *         can be a URL path (e.g. "/C:/test/file.ext").
 209	 * @return The directory name (without the final separator), or an empty
 210	 *         string if path is a filename.
 211	 */
 212	static public String getDirectoryName (String path) {
 213		String tmp = path.replace('\\', '/'); // Normalize separators (some path are mixed)
 214		int n = tmp.lastIndexOf('/');
 215		if ( n > 0 ) {
 216			return path.substring(0, n);
 217		}
 218		else {
 219			return "";
 220		}
 221	}
 222	
 223	/**
 224	 * Determines if a given path ends with a file name separator for the current platform.
 225	 * If not, the file separator is appended to the path.
 226	 * @param path the given path.
 227	 * @param forceForwardSlash true if the ending separator must be a forward slash.
 228	 * @return the given path ending with the file name separator.
 229	 */
 230	static public String ensureSeparator (String path,
 231		boolean forceForwardSlash)
 232	{
 233		if ( isEmpty(path) ) return path;
 234		if ( path.endsWith("/") ) return path;
 235		if ( path.endsWith(File.separator) ) {
 236			if ( forceForwardSlash ) {
 237				path = path.substring(0, path.length()-1);
 238			}
 239			else {
 240				return path;
 241			}
 242		}
 243		if ( forceForwardSlash ) {
 244			return path + "/";
 245		}
 246		else {
 247			return path + File.separator;
 248		}
 249	}
 250	
 251	/**
 252	 * Determines if a given path starts with a file name separator for the current platform.
 253	 * If not, the file separator is prefixed to the path.
 254	 * @param path the given path.
 255	 * @param forceForwardSlash true if the leading separator must be a forward slash.
 256	 * @return the given path starting with the file name separator.
 257	 */
 258	static public String ensureLeadingSeparator (String path,
 259			boolean forceForwardSlash)
 260		{
 261			if ( isEmpty(path) ) return path;
 262			if ( path.startsWith("/") ) return path;
 263			if ( path.startsWith(File.separator) ) {
 264				if ( forceForwardSlash ) {
 265					path = path.substring(1, path.length());
 266				}
 267				else {
 268					return path;
 269				}
 270			}
 271			if ( forceForwardSlash ) {
 272				return "/" + path;
 273			}
 274			else {
 275				return File.separator + path;
 276			}
 277		}
 278	
 279	/**
 280	 * Replaces unsupported characters in a given short file name (no directory path) with a given replacer.
 281	 * @param fileName the given short file name
 282	 * @param replacer the given replacer
 283	 * @return the given file name with fixed unsupported characters
 284	 */
 285	public static String fixFilename(String fileName, String replacer) {
 286		if (Util.isEmpty(fileName)) return "";
 287		if (Util.isEmpty(replacer)) return fileName;
 288		String regex = "[*:<>?\\\\/|]"; // TODO This regex is for Windows, add for Linux/Mac OS
 289		replacer = replacer.replaceAll(regex, "_"); // In case replacer contains unsupported chars
 290        return fileName.replaceAll(regex, replacer);
 291    }
 292	
 293	/**
 294	 * Removes exceeding separators in a given path. Normalizes the given path to contain OS-specific file separators.
 295	 * @param path the given path
 296	 * @return the fixed given path
 297	 */
 298	public static String fixPath(String path) {
 299		return fixPath(path, true);
 300	}
 301	
 302	/**
 303	 * Removes exceeding separators in a given path. Optionally normalizes the given path to contain OS-specific file separators.
 304	 * @param path the given path
 305	 * @param forceOsSeparators true to ensure the path contains only OS-specific separators
 306	 * @return the fixed given path
 307	 */
 308	public static String fixPath(String path, boolean forceOsSeparators) {
 309		final String FP_PREFIX = "file://";
 310		String res = path;
 311		boolean hasFileProtocol = res.startsWith(FP_PREFIX);
 312		if (hasFileProtocol) {
 313			res = res.substring(FP_PREFIX.length()); // Remove "file://" part not to replace "///" with "//"
 314		}
 315		
 316		res = res.replaceAll("[\\\\/]+", "/");
 317		
 318		if (!hasFileProtocol && forceOsSeparators) {
 319			res = res.replace("/", File.separator);
 320		}
 321		
 322//		if (res.startsWith("\\")) {
 323//			// Don't let a Windows path start with a back slash 
 324//			res = res.substring(1);
 325//		}
 326		
 327		return hasFileProtocol ? FP_PREFIX + res : res;
 328	}
 329	
 330	/**
 331	 * Replaces unsupported characters in a given short file name (no directory path) with underscore.
 332	 * @param fileName the given short file name
 333	 * @return the given file name with fixed unsupported characters
 334	 */
 335	public static String fixFilename(String fileName) {
 336		return fixFilename(fileName, "_");
 337	}	
 338	
 339	/**
 340	 * Builds a path from given parts. Path separators are normalized. 
 341	 * @param parts parts of the path
 342	 * @return path containing the given parts separated with a file name separator 
 343	 * for the current platform. 
 344	 */
 345	public static String buildPath(String... parts) {
 346		String res = null;
 347		for (final String part : parts) {
 348			String normalizedPart = part.replace("\\", "/"); // Normalize to slash
 349			
 350			if (res == null) {
 351				res = normalizedPart;
 352			}
 353			else {
 354				if (!res.endsWith("/")) {
 355					res += "/";
 356				}
 357				res += normalizedPart;
 358			}
 359		}
 360		return fixPath(res);
 361	}
 362	
 363	/**
 364	 * Return a list of files based on suffix (i.e, .xml, .html etc.)
 365	 * @param directory - directory where files are located
 366	 * @param suffix - the sufix used to filter files
 367	 * @return - list of files matching the suffix
 368	 * @throws URISyntaxException if the syntax is not correct.
 369	 */
 370	public static String[] getFilteredFiles(final String directory, final String suffix)
 371			throws URISyntaxException {
 372		File dir = new File(directory);
 373		FilenameFilter filter = new FilenameFilter() {
 374			public boolean accept(File dir, String name) {
 375				return name.endsWith(suffix);
 376			}
 377		};
 378		return dir.list(filter);
 379	}
 380	
 381	/**
 382	 * Creates the directory tree for the give full path (dir+filename)
 383	 * 
 384	 * @param path
 385	 *            directory to create and filename. If you want to pass only a directory
 386	 *            name make sure it has a trailing separator (e.g. "c:\project\tmp\").
 387	 *            The path can be a URL path (e.g. "/C:/test/file.ext").
 388	 * @return	true if one of more directories were created or if there was no directory to create,
 389	 *          false otherwise (an error occurred) 
 390	 */
 391	static public boolean createDirectories (String path) {
 392		String tmp = path.replace('\\', '/'); // Normalize separators (some path are mixed)
 393		int n = tmp.lastIndexOf('/');
 394		if ( n == -1 ) return true; // Nothing to do
 395		// Else, use the directory part and create the tree
 396		String dirPath = path.substring(0, n);
 397		File dir = new File(dirPath);
 398		if ( !dir.exists() ) return dir.mkdirs();
 399		else return true;
 400	}
 401	
 402	/**
 403	 * Escape newlines and whitespace so they survive roundtrip as an xml attribute
 404	 * \n=\u0098
 405	 * \r=\u0097
 406	 * \u0020=\u0096
 407	 * @param text - original text
 408	 * @return escaped string
 409	 */
 410	static public String escapeWhitespaceForXML (String text) {
 411		if ( text == null || text.isEmpty()) return "";
 412		StringBuffer sbTmp = new StringBuffer(text.length());
 413		char ch;
 414		for ( int i = 0; i < text.length(); i++ ) {
 415			ch = text.charAt(i);
 416			switch (ch) {
 417			case '\n':
 418				sbTmp.append('\u0098');
 419				continue;
 420			case '\r':
 421				sbTmp.append('\u0097');
 422				continue;
 423			case ' ':
 424				sbTmp.append('\u0096');
 425				continue;
 426			default:
 427				if ( text.charAt(i) > 127 ) { // Extended chars
 428					if ( Character.isHighSurrogate(ch) ) {
 429						int cp = text.codePointAt(i++);
 430						String tmp = new String(Character.toChars(cp));				
 431						sbTmp.append(tmp);				
 432					} else {
 433						sbTmp.append(text.charAt(i));
 434					}					
 435				} else { // ASCII chars
 436					sbTmp.append(text.charAt(i));
 437				}
 438				continue;
 439			}
 440		}		
 441		return sbTmp.toString();
 442	}
 443
 444	static public String unescapeWhitespaceForXML (String text) {
 445		if ( text == null || text.isEmpty()) return "";
 446		StringBuffer sbTmp = new StringBuffer(text.length());
 447		char ch;
 448		for ( int i = 0; i < text.length(); i++ ) {
 449			ch = text.charAt(i);
 450			switch (ch) {
 451			case '\u0098':
 452				sbTmp.append('\n');
 453				continue;
 454			case '\u0097':
 455				sbTmp.append('\r');
 456				continue;
 457			case '\u0096':
 458				sbTmp.append(' ');
 459				continue;
 460			default:
 461				if ( text.charAt(i) > 127 ) { // Extended chars
 462					if ( Character.isHighSurrogate(ch) ) {
 463						int cp = text.codePointAt(i++);
 464						String tmp = new String(Character.toChars(cp));				
 465						sbTmp.append(tmp);				
 466					} else {
 467						sbTmp.append(text.charAt(i));
 468					}					
 469				} else { // ASCII chars
 470					sbTmp.append(text.charAt(i));
 471				}
 472				continue;
 473			}
 474		}		
 475		return sbTmp.toString();
 476	}
 477
 478	/**
 479	 * Escapes a string for XML.
 480	 * 
 481	 * @param text
 482	 *            string to escape.
 483	 * @param quoteMode
 484	 *            0=no quote escaped, 1=apos and quot, 2=#39 and quot, and
 485	 *            3=quot only.
 486	 * @param escapeGT
 487	 *            true to always escape '&gt;' to gt
 488	 * @param encoder
 489	 *            the character set encoder to use to detect un-supported
 490	 *            character, or null to never escape normal characters.
 491	 * @return the escaped string.
 492	 */
 493	static public String escapeToXML (String text,
 494		int quoteMode,
 495		boolean escapeGT,
 496		CharsetEncoder encoder)
 497	{
 498		if ( text == null ) return "";
 499		StringBuffer sbTmp = new StringBuffer(text.length());
 500		char ch;
 501		for ( int i = 0; i < text.length(); i++ ) {
 502			ch = text.charAt(i);
 503			switch (ch) {
 504			case '<':
 505				sbTmp.append("&lt;");
 506				continue;
 507			case '>':
 508				if (escapeGT)
 509					sbTmp.append("&gt;");
 510				else {
 511					if (( i > 0 ) && ( text.charAt(i - 1) == ']' )) {
 512						sbTmp.append("&gt;");
 513					}
 514					else {
 515						sbTmp.append('>');
 516					}
 517				}
 518				continue;
 519			case '&':
 520				sbTmp.append("&amp;");
 521				continue;
 522			case '"':
 523				if ( quoteMode > 0 ) {
 524					sbTmp.append("&quot;");
 525				}
 526				else {
 527					sbTmp.append('"');
 528				}
 529				continue;
 530			case '\'':
 531				switch ( quoteMode ) {
 532				case 1:
 533					sbTmp.append("&apos;");
 534					break;
 535				case 2:
 536					sbTmp.append("&#39;");
 537					break;
 538				default:
 539					sbTmp.append(text.charAt(i));
 540					break;
 541				}
 542				continue;
 543			default:
 544				if ( text.charAt(i) > 127 ) { // Extended chars
 545					if ( Character.isHighSurrogate(ch) ) {
 546						int cp = text.codePointAt(i++);
 547						String tmp = new String(Character.toChars(cp));
 548						if (( encoder != null ) && !encoder.canEncode(tmp) ) {
 549							sbTmp.append(String.format("&#x%x;", cp));
 550						} else {
 551							sbTmp.append(tmp);
 552						}
 553					}
 554					else {
 555						if (( encoder != null ) && !encoder.canEncode(text.charAt(i)) ) {
 556							sbTmp.append(String.format("&#x%04x;", text.codePointAt(i)));
 557						}
 558						else { // No encoder or char is supported
 559							sbTmp.append(text.charAt(i));
 560						}
 561					}
 562				}
 563				else { // ASCII chars
 564					sbTmp.append(text.charAt(i));
 565				}
 566				continue;
 567			}
 568		}
 569		return sbTmp.toString();
 570	}
 571
 572	/**
 573	 * Escapes a given string into RTF format.
 574	 * 
 575	 * @param text
 576	 *            the string to convert.
 577	 * @param convertLineBreaks
 578	 *            Indicates if the line-breaks should be converted.
 579	 * @param lineBreakStyle
 580	 *            Type of line-break conversion. 0=do nothing special, 1=close
 581	 *            then re-open as external, 2=close then re-open as internal.
 582	 * @param encoder
 583	 *            Encoder to use for the extended characters.
 584	 * @return The input string escaped to RTF.
 585	 */
 586	static public String escapeToRTF (String text,
 587		boolean convertLineBreaks,
 588		int lineBreakStyle,
 589		CharsetEncoder encoder)
 590	{
 591		try {
 592			if ( text == null ) return "";
 593			StringBuffer tmp = new StringBuffer(text.length());
 594			CharBuffer tmpBuf = CharBuffer.allocate(1);
 595			ByteBuffer encBuf;
 596
 597			for ( int i=0; i<text.length(); i++ ) {
 598				switch ( text.charAt(i) ) {
 599				case '{':
 600				case '}':
 601				case '\\':
 602					tmp.append("\\").append(text.charAt(i));
 603					break;
 604				case '\r': // Skip
 605					break;
 606				case '\n':
 607					if ( convertLineBreaks ) {
 608						switch ( lineBreakStyle ) {
 609						case 1: // Outside external
 610							tmp.append(RTF_ENDCODE);
 611							tmp.append("\r\n\\par ");
 612							tmp.append(RTF_STARTCODE);
 613							continue;
 614						case 2:
 615							tmp.append(RTF_ENDINLINE);
 616							tmp.append("\r\n\\par ");
 617							tmp.append(RTF_STARTINLINE);
 618							continue;
 619						case 0: // Just convert
 620						default:
 621							tmp.append("\r\n\\par ");
 622							continue;
 623						}
 624					}
 625					else {
 626						tmp.append("\n");
 627					}
 628					break;
 629				case '\u00a0': // Non-breaking space
 630					tmp.append("\\~"); // No extra space (it's a control word)
 631					break;
 632				case '\t':
 633					tmp.append("\\tab ");
 634					break;
 635				case '\u2022':
 636					tmp.append("\\bullet ");
 637					break;
 638				case '\u2018':
 639					tmp.append("\\lquote ");
 640					break;
 641				case '\u2019':
 642					tmp.append("\\rquote ");
 643					break;
 644				case '\u201c':
 645					tmp.append("\\ldblquote ");
 646					break;
 647				case '\u201d':
 648					tmp.append("\\rdblquote ");
 649					break;
 650				case '\u2013':
 651					tmp.append("\\endash ");
 652					break;
 653				case '\u2014':
 654					tmp.append("\\emdash ");
 655					break;
 656				case '\u200d':
 657					tmp.append("\\zwj ");
 658					break;
 659				case '\u200c':
 660					tmp.append("\\zwnj ");
 661					break;
 662				case '\u200e':
 663					tmp.append("\\ltrmark ");
 664					break;
 665				case '\u200f':
 666					tmp.append("\\rtlmark ");
 667					break;
 668
 669				default:
 670					if ( text.charAt(i) > 127 ) {
 671						if ( encoder.canEncode(text.charAt(i)) ) {
 672							tmpBuf.put(0, text.charAt(i));
 673							tmpBuf.position(0);
 674							encBuf = encoder.encode(tmpBuf);
 675							if ( encBuf.limit() > 1 ) {
 676								tmp.append(String.format("{\\uc%d", encBuf.limit()));
 677								tmp.append(String.format("\\u%d", (int) text.charAt(i)));
 678								for (int j = 0; j < encBuf.limit(); j++) {
 679									tmp.append(String.format("\\'%x", (encBuf.get(j) < 0 ? (0xFF ^ ~encBuf.get(j))
 680										: encBuf.get(j))));
 681								}
 682								tmp.append("}");
 683							}
 684							else {
 685								tmp.append(String.format("\\u%d", (int) text.charAt(i)));
 686								tmp.append(String.format("\\'%x", (encBuf.get(0) < 0 ? (0xFF ^ ~encBuf.get(0))
 687									: encBuf.get(0))));
 688							}
 689						}
 690						else { // Cannot encode in the RTF encoding, so use
 691							// Just Unicode
 692							tmp.append(String.format("\\u%d ?", (int) text.charAt(i)));
 693						}
 694					}
 695					else {
 696						tmp.append(text.charAt(i));
 697					}
 698					break;
 699				}
 700			}
 701			return tmp.toString();
 702		}
 703		catch ( CharacterCodingException e ) {
 704			throw new OkapiException(e);
 705		}
 706	}
 707
 708	/**
 709	 * Recursive function to delete the content of a given directory (including
 710	 * all its sub-directories. This does not delete the original parent
 711	 * directory.
 712	 * @param directory the directory of the content to delete.
 713	 * @return true if the content was deleted, false otherwise.
 714	 */
 715	public static boolean deleteDirectory (File directory) {
 716		boolean res = true;
 717		File[] list = directory.listFiles();
 718		if ( list == null ) return true;
 719		for ( File f : list ) {
 720			if ( f.isDirectory() ) {
 721				deleteDirectory(f);
 722			}
 723			// Set the result, but keep deleting
 724			if ( !f.delete() ) res = false;
 725		}
 726		return res;
 727	}
 728
 729	/**
 730	 * Deletes the content of a given directory, and if requested, the directory
 731	 * itself. Sub-directories and their content are part of the deleted
 732	 * content.
 733	 * 
 734	 * @param directory
 735	 *            the path of the directory to delete
 736	 * @param contentOnly
 737	 *            indicates if the directory itself should be removed. If this
 738	 *            flag is true, only the content is deleted.
 739	 */
 740	public static void deleteDirectory (String directory,
 741		boolean contentOnly)
 742	{
 743		File f = new File(directory);
 744		// Make sure this is a directory
 745		if ( !f.isDirectory() ) {
 746			return;
 747		}
 748		deleteDirectory(f);
 749		if ( !contentOnly ) {
 750			f.delete();
 751		}
 752	}
 753
 754	/**
 755	 * Gets the filename of a path.
 756	 * 
 757	 * @param path
 758	 *            the path from where to get the filename. The path can be a URL
 759	 *            path (e.g. "/C:/test/file.ext").
 760	 * @param keepExtension
 761	 *            true to keep the existing extension, false to remove it.
 762	 * @return the filename with or without extension.
 763	 */
 764	static public String getFilename (String path,
 765		boolean keepExtension)
 766	{
 767		// Get the filename (allow path with mixed separators)
 768		int n = path.lastIndexOf('/');
 769		int n2 = path.lastIndexOf('\\');
 770		if ( n2 > n ) n = n2;
 771		if ( n == -1 ) { // Then try Windows
 772			n = path.lastIndexOf('\\');
 773		}
 774		if ( n > -1 ) {
 775			path = path.substring(n + 1);
 776		}
 777		// Stop here if we keep the extension
 778		if ( keepExtension ) {
 779			return path;
 780		}
 781		// Else: remove the extension if there is one
 782		n = path.lastIndexOf('.');
 783		if ( n > -1 ) {
 784			return path.substring(0, n);
 785		}
 786		// Else:
 787		return path;
 788	}
 789
 790	/**
 791	 * Gets the extension of a given path or filename.
 792	 * 
 793	 * @param path
 794	 *            the original path or filename.
 795	 * @return the last extension of the filename (including the period), or
 796	 *         empty if there is no period in the filename. If the filename ends
 797	 *         with a period, the return is a period.
 798	 *         Never returns null.
 799	 */
 800	static public String getExtension (String path) {
 801		// Get the extension
 802		int n = path.lastIndexOf('.');
 803		if (n == -1) return ""; // Empty
 804		return path.substring(n);
 805	}
 806
 807	/**
 808	 * Makes a URI string from a path. If the path itself can be recognized as a
 809	 * string URI already, it is passed unchanged. For example "C:\test" and
 810	 * "file:///C:/test" will both return "file:///C:/test" encoded as URI.
 811	 * 
 812	 * @param pathOrUri
 813	 *            the path to change to URI string.
 814	 * @return the URI string.
 815	 * @throws OkapiUnsupportedEncodingException if UTF-8 is not supported (can't happen).
 816	 */
 817	static public String makeURIFromPath (String pathOrUri) {
 818		if (isEmpty(pathOrUri)) {
 819			throw new IllegalArgumentException();
 820		}
 821		// This should catch most of the URI forms
 822		pathOrUri = pathOrUri.replace('\\', '/');
 823		if (pathOrUri.indexOf("://") != -1)
 824			return pathOrUri;
 825		// If not that, then assume it's a file
 826		if (pathOrUri.startsWith("file:/"))
 827			pathOrUri = pathOrUri.substring(6);
 828		if (pathOrUri.startsWith("/"))
 829			pathOrUri = pathOrUri.substring(1);
 830		String tmp = URLEncodeUTF8(pathOrUri);
 831		// Use '%20' instead of '+': '+ not working with File(uri) it seems
 832		return "file:///" + tmp.replace("+", "%20");
 833	}
 834	
 835	/**
 836	 * Creates a new URI object from a path or a URI string.
 837	 * 
 838	 * @param pathOrUri
 839	 *            the path or URI string to use.
 840	 * @return the new URI object for the given path or URI string.
 841	 */
 842	static public URI toURI (String pathOrUri) {
 843		try {
 844			// Satisfy unit test for empty path.
 845			if ( pathOrUri == null || pathOrUri.equals("") ) {
 846				return new URI("");
 847			}
 848			URI uri = new URI(pathOrUri);
 849			if ( uri != null && uri.isAbsolute() ) {
 850				return uri;
 851			}
 852		}
 853		catch ( URISyntaxException e ) {
 854			// If that didn't work, try going through File.
 855		}
 856		return new File(pathOrUri).toURI();
 857	}
 858
 859
 860	/**
 861	 * Creates a new URL object from a path or a URI string.  This is a
 862	 * convenience wrapper to catch the various conversion exceptions.
 863	 * 
 864	 * @param pathOrUri
 865	 *            the path or URI string to use.
 866	 * @return the new URI object for the given path or URI string.
 867	 */
 868	static public URL toURL(String pathOrUri) {
 869		return URItoURL(toURI(pathOrUri));
 870	}
 871
 872	/**
 873	 * Convert a URL to a URI. Convenience method to avoid catching
 874	 * {@link URISyntaxException} all over the place.
 875	 * @param url The URL to convert
 876	 * @return The new URI object for the given URL
 877	 */
 878	static public URI URLtoURI (URL url) {
 879		try {
 880			return url.toURI();
 881		} catch (URISyntaxException e) {
 882			throw new OkapiException(e);
 883		}
 884	}
 885
 886	/**
 887	 * Convert a URI to a URL.  Convenience method to avoid catching
 888	 * {@link MalformedURLException}.
 889	 * @param uri to convert
 890	 * @return the resulting url
 891	 */
 892	static public URL URItoURL(URI uri) {
 893		try {
 894			return uri.toURL();
 895		} catch (MalformedURLException e) {
 896			throw new OkapiException(e);
 897		}
 898	}
 899
 900	/**
 901	 * Gets the longest common path between an existing current directory and a
 902	 * new one.
 903	 * 
 904	 * @param currentDir
 905	 *            the current longest common path.
 906	 * @param newDir
 907	 *            the new directory to compare with.
 908	 * @param ignoreCase
 909	 *            true if the method should ignore cases differences.
 910	 * @return the longest sub-directory that is common to both directories.
 911	 *         This can be a null if the current directory is null,
 912	 *         or empty if there is no common path.
 913	 */
 914	static public String longestCommonDir (String currentDir,
 915		String newDir,
 916		boolean ignoreCase)
 917	{
 918		if ( currentDir == null ) {
 919			return newDir;
 920		}
 921		if ( currentDir.length() == 0 ) {
 922			return currentDir;
 923		}
 924
 925		// Get temporary copies
 926		String currentLow = currentDir;
 927		String newLow = newDir;
 928		if ( ignoreCase ) {
 929			currentLow = currentDir.toLowerCase();
 930			newLow = newDir.toLowerCase();
 931		}
 932
 933		// The new path equals, or include the existing root: no change
 934		if ( newLow.indexOf(currentLow) == 0 ) {
 935			return currentDir;
 936		}
 937
 938		// Search the common path
 939		String tmp = currentLow;
 940		int i = 0;
 941		while ( newLow.indexOf(tmp) != 0 ) {
 942			tmp = Util.getDirectoryName(tmp);
 943			i++;
 944			if ( tmp.length() == 0 ) {
 945				return ""; // No common path at all
 946			}
 947		}
 948
 949		// Do not return currentDir.substring(0, tmp.length());
 950		// because the lower-case string maybe of a different length than cased one
 951		// (e.g. German Sz). Instead re-do the splitting as many time as needed.
 952		tmp = currentDir;
 953		for ( int j = 0; j < i; j++ ) {
 954			tmp = Util.getDirectoryName(tmp);
 955		}
 956		return tmp;
 957	}
 958	
 959	/**
 960	 * Gets the longest common path between directories on a given list.
 961	 * 
 962	 * @param ignoreCase
 963	 *            if the method should ignore cases differences.
 964	 * @param directories
 965	 *            the given list of directories.
 966	 * @return the longest sub-directory that is common to all directories.
 967	 *         This can be a null or empty string.
 968	 */
 969	static public String longestCommonDir (boolean ignoreCase, String... directories) {
 970		if (directories == null) return "";
 971		if (directories.length == 1) return directories[0]; // Can be null
 972		
 973		String res = directories[0];
 974		for (int i = 1; i < directories.length; i++) {
 975			res = longestCommonDir(res, directories[i], ignoreCase);
 976		}
 977		return res;
 978	}
 979
 980	/**
 981	 * Indicates if the current OS is case-sensitive.
 982	 * 
 983	 * @return true if the current OS is case-sensitive, false if otherwise.
 984	 */
 985	static public boolean isOSCaseSensitive () {
 986		// May not work on all platforms,
 987		// But should on basic Windows, Mac and Linux
 988		// (Use Windows file separator-type to guess the OS)
 989		return !File.separator.equals("\\");
 990	}
 991
 992	/**
 993	 * Writes a Byte-Order-Mark if the encoding indicates it is needed. This
 994	 * methods must be the first call after opening the writer.
 995	 * 
 996	 * @param writer
 997	 *            writer where to output the BOM.
 998	 * @param bomOnUTF8
 999	 *            indicates if we should use a BOM on UTF-8 files.
1000	 * @param encoding
1001	 *            encoding of the output.
1002	 * @throws OkapiIOException if anything went wrong with the writing.
1003	 */
1004	static public void writeBOMIfNeeded (Writer writer,
1005		boolean bomOnUTF8,
1006		String encoding)
1007	{
1008		try {
1009			String tmp = encoding.toLowerCase();
1010
1011			// Check UTF-8 first (most cases)
1012			if ((bomOnUTF8) && (tmp.equalsIgnoreCase("utf-8"))) {
1013				writer.write("\ufeff");
1014				return;
1015			}
1016
1017			/*
1018			 * It seems writers do the following: For "UTF-16" they output
1019			 * UTF-16BE with a BOM. For "UTF-16LE" they output UTF-16LE without
1020			 * BOM. For "UTF-16BE" they output UTF-16BE without BOM. So we force a
1021			 * BOM for UTF-16LE and UTF-16BE
1022			 */
1023			if (tmp.equals("utf-16be") || tmp.equals("utf-16le")) {
1024				writer.write("\ufeff");
1025				return;
1026			}
1027			// TODO: Is this an issue? Does *reading* UTF-16LE/BE does not check
1028			// for BOM?
1029		} catch (IOException e) {
1030			throw new OkapiIOException(e);
1031		}
1032	}
1033
1034	/**
1035	 * Gets the default system temporary directory to use for the current user.
1036	 * The directory path returned has never a trailing separator.
1037	 * 
1038	 * @return The directory path of the temporary directory to use, without
1039	 *         trailing separator.
1040	 */
1041	public static String getTempDirectory () {
1042		String tmp = System.getProperty("java.io.tmpdir");
1043		// Normalize for all platforms: no trailing separator
1044		if (tmp.endsWith(File.separator)) // This separator is always
1045			// platform-specific
1046			tmp = tmp.substring(0, tmp.length() - 1);
1047		return tmp;
1048	}
1049
1050	/**
1051	 * Gets the text content of the first TEXT child of an element node. This is
1052	 * to use instead of node.getTextContent() which does not work with some
1053	 * Macintosh Java VMs. Note this work-around get <b>only the first TEXT
1054	 * node</b>.
1055	 * 
1056	 * @param node
1057	 *            the container element.
1058	 * @return the text of the first TEXT child node.
1059	 */
1060	public static String getTextContent (Node node) {
1061		Node tmp = node.getFirstChild();
1062		while (true) {
1063			if (tmp == null)
1064				return "";
1065			if (tmp.getNodeType() == Node.TEXT_NODE) {
1066				return tmp.getNodeValue();
1067			}
1068			tmp = tmp.getNextSibling();
1069		}
1070	}
1071
1072	/**
1073	 * Calculates safely a percentage. If the total is 0, the methods return 1.
1074	 * 
1075	 * @param part
1076	 *            the part of the total.
1077	 * @param total
1078	 *            the total.
1079	 * @return the percentage of part in total.
1080	 */
1081	public static int getPercentage (int part,
1082		int total)
1083	{
1084		return (total == 0 ? 1 : Math.round((float) part / (float) total * 100));
1085	}
1086
1087	/**
1088	 * Creates a string Identifier based on the hash code of the given text.
1089	 * 
1090	 * @param text
1091	 *		the text to make an ID for.
1092	 * @return The string identifier for the given text.
1093	 */
1094	public static String makeId (String text) {
1095		int n = text.hashCode();
1096		return String.format("%s%X", ((n > 0) ? 'P' : 'N'), n);
1097	}
1098
1099	/**
1100	 * Indicates if two language codes are 'the same'. The comparison ignores
1101	 * case differences, and if the parameter ignoreRegion is true, any part
1102	 * after the first '-' is also ignored. Note that the character '_' is
1103	 * treated like a character '-'.
1104	 * 
1105	 * @param lang1
1106	 *            first language code to compare.
1107	 * @param lang2
1108	 *            second language code to compare.
1109	 * @param ignoreRegion
1110	 *            True to ignore any part after the first separator, false to
1111	 *            take it into account.
1112	 * @return true if, according the given options, the two language codes are
1113	 *         the same. False otherwise.
1114	 */
1115	static public boolean isSameLanguage (String lang1,
1116		String lang2,
1117		boolean ignoreRegion)
1118	{
1119		lang1 = lang1.replace('_', '-');
1120		lang2 = lang2.replace('_', '-');
1121		if (ignoreRegion) { // Do not take the region part into account
1122			int n = lang1.indexOf('-');
1123			if (n > -1) {
1124				lang1 = lang1.substring(0, n);
1125			}
1126			n = lang2.indexOf('-');
1127			if (n > -1) {
1128				lang2 = lang2.substring(0, n);
1129			}
1130		}
1131		return lang1.equalsIgnoreCase(lang2);
1132	}
1133
1134	/**
1135	 * Indicates if a given string is null or empty.
1136	 * 
1137	 * @param string
1138	 *            the string to check.
1139	 * @return true if the given string is null or empty.
1140	 */
1141	static public boolean isEmpty (String string) {
1142		return (( string == null ) || ( string.length() == 0 ));
1143	}
1144
1145	/**
1146	 * Indicates if a locale id is null or empty.
1147	 * @param locale the locale id to examine.
1148	 * @return true if the given locale id is null or empty, false otherwise.
1149	 */
1150	static public boolean isNullOrEmpty (LocaleId locale) {
1151		return (( locale == null ) || ( locale.equals(LocaleId.EMPTY) ));
1152	}
1153	
1154	/**
1155	 * Indicates if a string is null or empty, optionally ignoring the white spaces.
1156	 * @param string the string to examine.
1157	 * @param ignoreWS true to ignore white spaces.
1158	 * @return true if the given string is null, or empty. The argument ignoreWS is true a string
1159	 * with only white spaces is concidered empty.
1160	 */
1161	static public boolean isEmpty (String string,
1162		boolean ignoreWS)
1163	{
1164        if ( ignoreWS && ( string != null )) {
1165            string = string.trim();
1166        }
1167		return isEmpty(string);
1168	}
1169	
1170	/**
1171	 * Indicates if a StringBuilder object is null or empty.
1172	 * @param sb the object to examine.
1173	 * @return true if the given object is null or empty. 
1174	 */
1175	static public boolean isEmpty (StringBuilder sb) {
1176		return (( sb == null ) || ( sb.length() == 0 ));
1177	}
1178
1179	/**
1180	 * Indicates if a given list is null or empty.
1181	 * @param <E> the type of the elements in the list.
1182	 * @param e the list to examine.
1183	 * @return true if the list is null or empty.
1184	 */
1185	static public <E> boolean isEmpty (List <E> e) {
1186		return (( e == null ) || e.isEmpty() );
1187	}
1188	
1189	/**
1190	 * Indicates if a given map is null or empty.
1191	 * @param <K> the type of the map's keys.
1192	 * @param <V> the type of the map's values.
1193	 * @param map the map to examine.
1194	 * @return true if the map is null or empty,
1195	 */
1196	public static <K, V> boolean isEmpty (Map<K, V> map) {		
1197		return (( map == null ) || map.isEmpty() );
1198	}
1199	
1200	/**
1201	 * Indicates if an array is null or empty.
1202	 * @param e the array to examine.
1203	 * @return true if the given array is null or empty.
1204	 */
1205	static public boolean isEmpty (Object[] e) {
1206		return (e == null ||(e != null && e.length == 0));
1207	}
1208	
1209	/**
1210	 * Gets the length of a string, even a null one.
1211	 * @param string the string to examine.
1212	 * @return the length of the given string, 0 if the string is null.
1213	 */
1214	static public int getLength (String string) {
1215		return (isEmpty(string)) ? 0 : string.length();
1216	}
1217
1218	/**
1219	 * Gets a character at a given position in a string.
1220	 * The string can be null and the position can be beyond the last character.
1221	 * @param string the string to examine.
1222	 * @param pos the position of the character to retrieve. 
1223	 * @return the character at the given position,
1224	 * or '\0' if the string is null or if the position is beyond the length of the string. 
1225	 */
1226	static public char getCharAt (String string,
1227		int pos)
1228	{
1229		if ( isEmpty(string) ) {
1230			return '\0';
1231		}
1232		return (string.length() > pos) ? string.charAt(pos) : '\0';
1233	}
1234
1235	/**
1236	 * Gets the last character of a given string.
1237	 * The string can be null or empty.
1238	 * @param string the string to examine.
1239	 * @return the last character of the given string,
1240	 * or '\0' if the string is null or empty.
1241	 */
1242	static public char getLastChar (String string) {
1243		if ( isEmpty(string) ) {
1244			return '\0';
1245		}
1246		return string.charAt(string.length() - 1);
1247	}
1248
1249	/**
1250	 * Deletes the last character of a given string.
1251	 * The string can be null or empty.
1252	 * @param string the string where to remove the character.
1253	 * @return a new string where the last character has been removed,
1254	 * or an empty string if the given string was null or empty. 
1255	 */
1256	static public String deleteLastChar (String string) {
1257		if ( isEmpty(string) ) {
1258			return "";
1259		}
1260		return string.substring(0, string.length() - 1);
1261	}
1262
1263	/**
1264	 * Gets the last character of a given StringBuilder object.
1265	 * The object can be null or empty.
1266	 * @param sb the StringBuilder object to examine.
1267	 * @return the last character of the given StringBuilder object,
1268	 * or '\0' if the object is null or empty. 
1269	 */
1270	static public char getLastChar (StringBuilder sb) {
1271		if ( isEmpty(sb) ) {
1272			return '\0';
1273		}
1274		return sb.charAt(sb.length() - 1);
1275	}
1276
1277	/**
1278	 * Deletes the last character of a given StringBuilder object.
1279	 * If the object is null or empty no character are removed.
1280	 * @param sb the StringBuilder object where to remove the character.
1281	 */
1282	static public void deleteLastChar (StringBuilder sb) {
1283		if ( isEmpty(sb) ) {
1284			return;
1285		}
1286		sb.deleteCharAt(sb.length() - 1);
1287	}
1288
1289	/**
1290	 * Indicates if a given index is within the list bounds.
1291	 * @param <E> the type of the list's elements.
1292	 * @param index the given index.
1293	 * @param list the given list.
1294	 * @return true if a given index is within the list bounds.
1295	 */
1296	public static <E> boolean checkIndex (int index,
1297		List<E> list)
1298	{
1299		return (list != null) && (index >= 0) && (index < list.size());
1300	}
1301
1302	/**
1303	 * Converts an integer value to a string.
1304	 * This method simply calls <code>String.valueOf(intValue);</code>.
1305	 * @param value the value to convert.
1306	 * @return the string representation of the given value.
1307	 */
1308	public static String intToStr (int value) {
1309		return String.valueOf(value);
1310	}
1311	
1312	/**
1313	 * Converts a string to an integer. If the conversion fails the method
1314	 * returns the given default value.
1315	 * @param value the string to convert.
1316	 * @param intDefault the default value to use if the conversion fails.
1317	 * @return the integer value of the string, or the provided default
1318	 * value if the conversion failed.
1319	 */
1320	public static int strToInt (String value,
1321		int intDefault)
1322	{
1323		if ( Util.isEmpty(value) ) {
1324			return intDefault;
1325		}
1326		try {
1327			return Integer.valueOf(value);
1328		}
1329		catch (NumberFormatException e) {
1330			return intDefault; 
1331		}
1332	}
1333	
1334	/**
1335	 * Convert String to int .
1336	 * Almost 3x faster than Integer.valueOf()
1337	 * @param s - string to be converted to int
1338	 * @return int represented by string
1339	 * @throws NumberFormatException if the {@link String} does not represent a number.
1340	 */
1341	public static int fastParseInt(final String s)
1342	{
1343		if (s == null)
1344			throw new NumberFormatException("Null string");
1345
1346		// Check for a sign.
1347		int num = 0;
1348		int sign = -1;
1349		final int len = s.length();
1350		final char ch = s.charAt(0);
1351		if (ch == '-')
1352		{
1353			if (len == 1)
1354				throw new NumberFormatException("Missing digits:  " + s);
1355			sign = 1;
1356		}
1357		else
1358		{
1359			final int d = ch - '0';
1360			if (d < 0 || d > 9)
1361				throw new NumberFormatException("Malformed:  " + s);
1362			num = -d;
1363		}
1364
1365		// Build the number.
1366		final int max = (sign == -1) ?
1367				-Integer.MAX_VALUE : Integer.MIN_VALUE;
1368		final int multmax = max / 10;
1369		int i = 1;
1370		while (i < len)
1371		{
1372			int d = s.charAt(i++) - '0';
1373			if (d < 0 || d > 9)
1374				throw new NumberFormatException("Malformed:  " + s);
1375			if (num < multmax)
1376				throw new NumberFormatException("Over/underflow:  " + s);
1377			num *= 10;
1378			if (num < (max + d))
1379				throw new NumberFormatException("Over/underflow:  " + s);
1380			num -= d;
1381		}
1382
1383		return sign * num;
1384	}
1385	
1386	/**
1387	 * Converts a string to a long. If the conversion fails the method
1388	 * returns the given default value.
1389	 * @param value the string to convert.
1390	 * @param longDefault the default value to use if the conversion fails.
1391	 * @return the long value of the string, or the provided default
1392	 * value if the conversion failed.
1393	 */
1394	public static long strToLong (String value,
1395		long longDefault)
1396	{
1397		if ( Util.isEmpty(value) ) {
1398			return longDefault;
1399		}
1400		try {
1401			return Long.valueOf(value);
1402		}
1403		catch (NumberFormatException e) {
1404			return longDefault; 
1405		}
1406	}
1407	
1408	/**
1409	 * Converts a string to a double. If the conversion fails the method
1410	 * returns the given default value.
1411	 * @param value the string to convert.
1412	 * @param doubleDefault the default value to use if the conversion fails.
1413	 * @return the double value of the string, or the provided default
1414	 * value if the conversion failed.
1415	 */
1416	public static double strToDouble (String value,
1417		double doubleDefault)
1418	{
1419		if ( Util.isEmpty(value) ) {
1420			return doubleDefault;
1421		}
1422		try {
1423			return Double.valueOf(value);
1424		}
1425		catch (NumberFormatException e) {
1426			return doubleDefault; 
1427		}
1428	}
1429
1430	/**
1431	 * Gets the element of an array for a given index.
1432	 * the method returns null if the index is out of bounds.
1433	 * @param <T> the type of the array's elements.
1434	 * @param array the array where to lookup the element.
1435	 * @param index the index.
1436	 * @return the element of the array for the given index, or null if the
1437	 * index is out of bounds, or if the element is null.
1438	 */
1439	public static <T>T get(T[] array,
1440		int index)
1441	{
1442		if (( index >= 0 ) && ( index < array.length )) {
1443			return array[index];
1444		}
1445		else {
1446			return null;
1447		}
1448	}
1449
1450	/**
1451	 * Returns true if a given index is within the array bounds.
1452	 * @param <T> the type of the array's elements.
1453	 * @param index the given index.
1454	 * @param array the given list.
1455	 * @return true if a given index is within the array bounds.
1456	 */
1457	public static <T> boolean checkIndex (int index,
1458		T[] array)
1459	{
1460		return (array != null) && (index >= 0) && (index < array.length);
1461	}
1462	
1463	/**
1464	 * Indicates whether a byte-flag is set or not in a given value. 
1465	 * @param value the value to check.
1466	 * @param flag the flag to look for.
1467	 * @return true if the flag is set, false if it is not.
1468	 */
1469	public static boolean checkFlag (int value,
1470		int flag)
1471	{
1472		return (value & flag) == flag;
1473	}
1474	
1475	/**
1476	 * Get the operating system
1477	 * @return one of WINDOWS, MAC or LINUX
1478	 */
1479	public static SUPPORTED_OS getOS () {
1480		String osName = System.getProperty("os.name");
1481		if (osName.startsWith("Windows")) { // Windows case
1482			return SUPPORTED_OS.WINDOWS;
1483		}		
1484		else if (osName.contains("OS X")) { // Macintosh case
1485			return SUPPORTED_OS.MAC;
1486		}
1487		return SUPPORTED_OS.LINUX;
1488	}
1489		
1490   /**
1491    * Opens the specified page in a web browser (Java 1.5 compatible).
1492    * <p>This is based on the public domain class BareBonesBrowserLaunch from Dem Pilafian at
1493    * (<a href="http://www.centerkey.com/java/browser/">www.centerkey.com/java/browser</a>)
1494    * @param url the URL of the page to open.
1495    */
1496	public static void openURL (String url) {
1497		String osName = System.getProperty("os.name");
1498		try {
1499			if (osName.contains("OS X")) { // Macintosh case
1500				/* One possible way. But this causes warning when run with -XstartOnFirstThread option
1501				Class<?> fileMgr = Class.forName("com.apple.eio.FileManager");
1502				Method openURL = fileMgr.getDeclaredMethod("openURL", new Class[]{String.class});
1503				openURL.invoke(null, new Object[] {url}); */
1504				// So, use open, and seems to work on the Macintosh (not Linux)
1505				Runtime.getRuntime().exec("open " + url);
1506			}
1507			else if (osName.startsWith("Windows")) { // Windows case
1508				Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
1509			}
1510			else { // Assumes Unix or Linux
1511				boolean found = false;
1512				for ( String browser : browsers ) {
1513					if ( !found ) { // Search for the first browser available
1514						found = Runtime.getRuntime().exec(new String[] {"which", browser}).waitFor() == 0;
1515						if ( found ) { // Start it if we find one
1516							Runtime.getRuntime().exec(new String[] {browser, url});
1517						}
1518					}
1519				}
1520				if ( !found ) {
1521					throw new Exception("No browser found.");
1522				}
1523			}
1524		}
1525		catch ( Throwable e ) {
1526			throw new OkapiException("Error attempting to launch web browser.", e);
1527		}
1528	}
1529	
1530	/**
1531	 * Opens a given topic of the OkapiWiki.
1532	 * @param topic the title of the topic/page.
1533	 */
1534	public static void openWikiTopic (String topic) {
1535		try {
1536			// Resolve spaces
1537			topic = topic.replace(' ', '_');
1538			//TODO: get the base URL from a properties file
1539			Util.openURL(new URL(String.format("http://okapiframework.org/wiki/index.php?title=%s", topic)).toString());
1540		}
1541		catch ( MalformedURLException e ) {
1542			e.printStackTrace();
1543		}
1544	}
1545
1546	/**
1547	 * Gets the directory location of a given class. The value returned can be the directory
1548	 * where the .class file is located, or, if the class in a JAR file, the directory
1549	 * where the .jar file is located.    
1550	 * @param theClass the class to query.
1551	 * @return the directory location of the given class, or null if an error occurs.
1552	 */
1553	public static String getClassLocation (Class<?> theClass) {
1554		String res = null;
1555		File file = new File(theClass.getProtectionDomain().getCodeSource().getLocation().getFile());
1556		res = URLDecodeUTF8(file.getAbsolutePath());
1557		// Remove the JAR file if necessary
1558		if ( res.endsWith(".jar") ) {
1559			res = getDirectoryName(res);
1560		}
1561		return res;
1562	}
1563	
1564
1565// Unused
1566//	/**
1567//	 * Generate a random string consisting only of numbers 
1568//	 * @param length - specifies length of the string
1569//	 * @return a random String
1570//	 */
1571//	public static String generateRandomId(int length) {
1572//		Random rnd = new Random();
1573//
1574//		StringBuilder sb = new StringBuilder( length );
1575//		  for( int i = 0; i < length; i++ ) { 
1576//			  sb.append(rnd.nextInt(9));
1577//		  }
1578//		  return sb.toString();
1579//
1580//	}
1581
1582	/**
1583	 * Replaces in a given original string, a potential variable ROOT_DIRECTORY_VAR by a given root directory.
1584	 * @param original the original string where to perform the replacement.
1585	 * @param rootDir the root directory. If null it will be automatically set to the
1586	 * user home directory.
1587	 * @return the original string with ROOT_DIRECTORY_VAR replaced if it was there.
1588	 */
1589	public static String fillRootDirectoryVariable (String original,
1590		String rootDir)
1591	{
1592		if ( rootDir == null ) {
1593			if ( !original.contains(ROOT_DIRECTORY_VAR) ) {
1594				// We early-out here if no work needs to be done so as to avoid
1595				// unnecessarily throwing when resolving user.dir.
1596				return original;
1597			} else {
1598				// The user.dir system property is restricted in some scenarios.
1599				// See "Forbidden System Properties":
1600				// https://docs.oracle.com/javase/tutorial/deployment/doingMoreWithRIA/properties.html
1601				// However we know that we need to replace ROOT_DIRECTORY_VAR
1602				// now, and the path will be useless without replacement, so we
1603				// just throw and let the caller handle it.
1604				rootDir = System.getProperty("user.dir");
1605			}
1606		}
1607		return original.replace(ROOT_DIRECTORY_VAR, rootDir);
1608	}
1609
1610	/**
1611	 * Replaces in a given original string, a potential variable INPUT_ROOT_DIRECTORY_VAR by a given root directory.
1612	 * @param original the original string where to perform the replacement.
1613	 * @param inputRootDir the input root directory. If null it will be automatically set to
1614	 * an empty string.
1615	 * @return the original string with INPUT_ROOT_DIRECTORY_VAR replaced if it was there.
1616	 */
1617	public static String fillInputRootDirectoryVariable (String original,
1618		String inputRootDir)
1619	{
1620		if ( inputRootDir == null ) {
1621			inputRootDir = "";
1622		}
1623		return original.replace(INPUT_ROOT_DIRECTORY_VAR, inputRootDir);
1624	}
1625	
1626	/**
1627	 * Expands environment variables by replacing strings of the type
1628	 * ${varname} with their values as reported by the system.
1629	 * @param original The original string in which to perform the replacement
1630	 * @return The original string with all environment variables expanded
1631	 */
1632	public static String fillSystemEnvars(String original)
1633	{
1634		for (Entry<String, String> e : System.getenv().entrySet()) {
1635			original = original.replace(String.format("${%s}", e.getKey()), e.getValue());
1636		}
1637		return original;
1638	}
1639	
1640	/**
1641	 * Check a piece of text to make sure that all contained variables (${foo})
1642	 * are resolvable by the fill...() methods in this class.
1643	 * @param text The text to check
1644	 * @param allowEnvar Whether or not to allow system environment variables
1645	 * @param allowRootDir Whether or not to allow ${rootDir}
1646	 * @param allowInputRootDir Whether or not to allow ${inputRootDir}
1647	 * @return Whether or not the input text's variables are valid
1648	 */
1649	public static boolean validateVariables(String text,
1650			boolean allowEnvar, boolean allowRootDir, boolean allowInputRootDir) {
1651		Matcher m = VARIABLE_REGEX_PATTERN.matcher(text);
1652		while (m.find()) {
1653			String var = m.group();
1654			String varName = m.group(1);
1655			if (allowEnvar && System.getenv().containsKey(varName)) continue;
1656			if (allowRootDir && var.equals(ROOT_DIRECTORY_VAR)) continue;
1657			if (allowInputRootDir && var.equals(INPUT_ROOT_DIRECTORY_VAR)) continue;
1658			return false;
1659		}
1660		return true;
1661	}
1662	
1663	/**
1664	 * Expand all supported variables and canonicalize (resolve ".." and ".")
1665	 * a path. If the input path has no variables, it is returned unchanged.
1666	 * rootDir and inputRootDir can be null.
1667	 * @param path The path to expand
1668	 * @param rootDir The directory to expand ${rootDir} into
1669	 * @param inputRootDir The directory to expand ${inputRootDir} into
1670	 * @return The expanded path
1671	 * @throws IOException If canonicalizing fails
1672	 */
1673	public static String expandPath (String path, String rootDir, String inputRootDir)
1674			throws IOException {
1675		if (!path.contains("${")) return path;
1676		
1677		path = Util.fillSystemEnvars(path);
1678		path = Util.fillRootDirectoryVariable(path, rootDir);
1679		path = Util.fillInputRootDirectoryVariable(path, inputRootDir);
1680		path = new File(path).getCanonicalPath();
1681		return path;
1682	}
1683
1684	/**
1685	 * Returns the smallest value in a given array of values.
1686	 * @param values the given array
1687	 * @return the smallest value in the array
1688	 */
1689	public static int min (int... values) {
1690		int res = Integer.MAX_VALUE;
1691		for (int value : values) {
1692			res = Math.min(res, value);
1693		}
1694		return (values.length > 0) ? 

Large files files are truncated, but you can click here to view the full file