/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package edu.mit.csail.sdg.alloy4;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.prefs.Preferences;
import edu.mit.csail.sdg.alloy4.ConstList.TempList;
/** This provides useful static methods for I/O and XML operations.
*
* <p><b>Thread Safety:</b> Safe.
*/
public final class Util {
/** This constructor is private, since this utility class never needs to be instantiated. */
private Util() { }
/** This reads and writes String-valued Java persistent preferences.
* <p><b>Thread Safety:</b> Safe.
*/
public static final class StringPref {
/** The id associated with this preference. */
private final String id;
/** The default value for this preference. */
private final String defaultValue;
/** Constructs a new StringPref object with the given id. */
public StringPref (String id) {this.id=id; this.defaultValue="";}
/** Constructs a new StringPref object with the given id and the given default value. */
public StringPref (String id, String defaultValue) {this.id=id; this.defaultValue=defaultValue;}
/** Sets the value for this preference. */
public void set (String value) { Preferences.userNodeForPackage(Util.class).put(id, value); }
/** Reads the value for this preference; if not set or is empty, we return the default value. */
public String get () {
String ans=Preferences.userNodeForPackage(Util.class).get(id, "");
return (ans==null || ans.length()==0) ? defaultValue : ans;
}
}
/** This reads and writes boolean-valued Java persistent preferences.
* <p><b>Thread Safety:</b> Safe.
*/
public static final class BooleanPref {
/** The id associated with this preference. */
private final String id;
/** Constructurs a new BooleanPref object with the given id. */
public BooleanPref (String id) { this.id=id; }
/** Sets the value for this preference. */
public void set (boolean value) { Preferences.userNodeForPackage(Util.class).put(id, value ? "y" : ""); }
/** Reads the value for this preference; if not set, we return false. */
public boolean get () { return "y".equals(Preferences.userNodeForPackage(Util.class).get(id, "")); }
}
/** This reads and writes integer-valued Java persistent preferences.
* <p><b>Thread Safety:</b> Safe.
*/
public static final class IntPref {
/** The id associated with this preference. */
private final String id;
/** The minimum value for this preference. */
private final int min;
/** The maximum value for this preference. */
private final int max;
/** The default value for this preference. */
private final int def;
/** If min>n, we return min; else if n>max, we return max; otherwise we return n. */
private int bound (int n) { return n<min ? min : (n>max? max : n); }
/** Make a new IntPref object with the given id; you must ensure max >= min, but def does not have to be between min..max */
public IntPref (String id, int min, int def, int max) {this.id=id; this.min=min; this.def=def; this.max=max;}
/** Sets the value for this preference. */
public void set (int value) { Preferences.userNodeForPackage(Util.class).putInt(id, bound(value)); }
/** Reads the value for this preference; if not set, we return the default value. */
public int get () {
int n;
String t = Preferences.userNodeForPackage(Util.class).get(id, "");
if (t==null || t.length()==0) return def;
try { n=Integer.parseInt(t); } catch(NumberFormatException ex) { return def; }
return bound(n);
}
}
/** Copy the input list, append "element" to it, then return the result as an unmodifiable list. */
public static<T> ConstList<T> append(List<T> list, T element) {
TempList<T> ans = new TempList<T>(list.size()+1);
ans.addAll(list).add(element);
return ans.makeConst();
}
/** Copy the input array, append "element" to it, then return the result as a new array. */
@SuppressWarnings("unchecked")
public static<T> T[] append(T[] list, T element) {
T[] ans = (T[]) java.lang.reflect.Array.newInstance(list.getClass().getComponentType(), list.length+1);
System.arraycopy(list, 0, ans, 0, list.length);
ans[ans.length-1] = element;
return ans;
}
/** Copy the input list, prepend "element" to it, then return the result as an unmodifiable list. */
public static<T> ConstList<T> prepend(List<T> list, T element) {
TempList<T> ans = new TempList<T>(list.size()+1);
ans.add(element).addAll(list);
return ans.makeConst();
}
/** Returns an unmodifiable List with same elements as the array. */
public static<T> ConstList<T> asList(T... array) {
return (new TempList<T>(array)).makeConst();
}
/** Returns a newly created LinkedHashSet containing the given elements in the given order. */
public static<V> LinkedHashSet<V> asSet(V... values) {
LinkedHashSet<V> ans = new LinkedHashSet<V>();
for(int i=0; i<values.length; i++) ans.add(values[i]);
return ans;
}
/** Returns a newly created LinkedHashMap mapping each key to its corresponding value, in the given order. */
public static<K,V> LinkedHashMap<K,V> asMap(K[] keys, V... values) {
LinkedHashMap<K,V> ans = new LinkedHashMap<K,V>();
for(int i=0; i<keys.length && i<values.length; i++) ans.put(keys[i], values[i]);
return ans;
}
/** Return an iterable whose iterator is a read-only iterator that first iterate over Collection1, and then Collection2. */
public static<E> Iterable<E> fastJoin(final Iterable<E> collection1, final Iterable<E> collection2) {
return new Iterable<E>() {
public Iterator<E> iterator() {
return new Iterator<E> () {
private Iterator<E> a=collection1.iterator(), b=collection2.iterator();
public boolean hasNext() {
if (a!=null) { if (a.hasNext()) return true; a=null; }
if (b!=null) { if (b.hasNext()) return true; b=null; }
return false;
}
public E next() {
if (a!=null) { if (a.hasNext()) return a.next(); a=null; }
if (b!=null) { if (b.hasNext()) return b.next(); b=null; }
throw new NoSuchElementException();
}
public void remove() { throw new UnsupportedOperationException(); }
};
}
};
}
/** Converts Windows/Mac/Unix linebreaks into '\n', and replace non-tab non-linebreak control characters into space. */
public static String convertLineBreak(String input) {
return input.replace("\r\n","\n").replace('\r','\n').replaceAll("[\000-\010\013\014\016-\037]"," ");
}
/** Attempt to close the file/stream/reader/writer and return true if and only if we successfully closed it.
* (If object==null, we return true right away)
*/
public static boolean close(Closeable object) {
if (object==null) return true;
boolean ans=true;
try {
if (object instanceof PrintStream && ((PrintStream)object).checkError()) ans=false;
if (object instanceof PrintWriter && ((PrintWriter)object).checkError()) ans=false;
object.close();
return ans;
} catch(Throwable ex) {
return false;
}
}
/** This synchronized field stores the current "default directory" which is used by the FileOpen and FileSave dialogs. */
private static String currentDirectory = null;
/** Modifies the current "default directory" which is used by the FileOpen and FileSave dialogs. */
public synchronized static void setCurrentDirectory(File newDirectory) {
if (newDirectory==null) // this can actually happen
currentDirectory = canon(System.getProperty("user.home"));
else
currentDirectory = canon(newDirectory.getAbsolutePath());
}
/** Returns the current "default directory" which is used by the FileOpen and FileSave dialogs. */
public synchronized static String getCurrentDirectory() {
if (currentDirectory == null) currentDirectory = canon(System.getProperty("user.home"));
return currentDirectory;
}
/** This returns the constant prefix to denote whether Util.readAll() should read from a JAR or read from the file system.
* (The reason we made this into a "method" rather than a constant String is that it is used
* by Util.canon() which is called by many static initializer blocks... so if we made this into a static field
* of Util, then it may not be initialized yet when we need it!)
*/
public static String jarPrefix() { return File.separator + "$alloy4$" + File.separator; }
/** Read everything into a String; throws IOException if an error occurred.
* (If filename begins with Util.jarPrefix() then we read from the JAR instead)
*/
public static String readAll(String filename) throws FileNotFoundException, IOException {
String JAR = jarPrefix();
boolean fromJar=false;
if (filename.startsWith(JAR)) { fromJar=true; filename=filename.substring(JAR.length()).replace('\\', '/'); }
InputStream fis=null;
int now=0, max=4096;
if (!fromJar) {
long maxL = new File(filename).length();
max = (int)maxL;
if (max != maxL) throw new IOException("File too big to fit in memory");
}
byte[] buf;
try {
buf = new byte[max];
fis = fromJar ? Util.class.getClassLoader().getResourceAsStream(filename) : new FileInputStream(filename);
if (fis==null) throw new FileNotFoundException("File \""+filename+"\" cannot be found");
while(true) {
if (now >= max) {
max = now + 4096;
if (max<now) throw new IOException("File too big to fit in memory");
byte[] buf2 = new byte[max];
if (now>0) System.arraycopy(buf, 0, buf2, 0, now);
buf = buf2;
}
int r = fis.read(buf, now, max-now);
if (r<0) break;
now = now + r;
}
} catch(OutOfMemoryError ex) {
System.gc();
throw new IOException("There is insufficient memory.");
} finally {
close(fis);
}
CodingErrorAction r = CodingErrorAction.REPORT;
CodingErrorAction i = CodingErrorAction.IGNORE;
ByteBuffer bbuf;
String ans = "";
try {
// We first try UTF-8;
bbuf=ByteBuffer.wrap(buf, 0, now);
ans=Charset.forName("UTF-8").newDecoder().onMalformedInput(r).onUnmappableCharacter(r).decode(bbuf).toString();
} catch(CharacterCodingException ex) {
try {
// if that fails, we try using the platform's default charset
bbuf=ByteBuffer.wrap(buf, 0, now);
ans=Charset.defaultCharset().newDecoder().onMalformedInput(r).onUnmappableCharacter(r).decode(bbuf).toString();
} catch(CharacterCodingException ex2) {
// if that also fails, we try using "ISO-8859-1" which should always succeed but may map some characters wrong
bbuf=ByteBuffer.wrap(buf, 0, now);
ans=Charset.forName("ISO-8859-1").newDecoder().onMalformedInput(i).onUnmappableCharacter(i).decode(bbuf).toString();
}
}
return convertLineBreak(ans);
}
/** Open then overwrite the file with the given content; throws Err if an error occurred. */
public static long writeAll(String filename, String content) throws Err {
final FileOutputStream fos;
try {
fos=new FileOutputStream(filename);
} catch(IOException ex) {
throw new ErrorFatal("Cannot write to the file "+filename);
}
// Convert the line break into the UNIX line break, and remove ^L, ^F... and other characters that confuse JTextArea
content = convertLineBreak(content);
// If the last line does not have a LINEBREAK, add it
if (content.length()>0 && content.charAt(content.length()-1)!='\n') content=content+"\n";
// Now, convert the line break into the local platform's line break, then write it to the file
try {
final String NL = System.getProperty("line.separator");
byte[] array = content.replace("\n",NL).getBytes("UTF-8");
fos.write(array);
fos.close();
return array.length;
} catch(IOException ex) {
close(fos);
throw new ErrorFatal("Cannot write to the file "+filename, ex);
}
}
/** Returns the canonical absolute path for a file.
* If an IO error occurred, or if the file doesn't exist yet,
* we will at least return a noncanonical but absolute path for it.
* <p> Note: if filename=="", we return "".
*/
public static final String canon(String filename) {
if (filename==null || filename.length()==0) return "";
if (filename.startsWith(jarPrefix())) {
char sep = File.separatorChar, other = (sep=='/' ? '\\' : '/');
return filename.replace(other, sep);
}
File file = new File(filename);
try { return file.getCanonicalPath(); } catch(IOException ex) { return file.getAbsolutePath(); }
}
/** Sorts two strings for optimum module order; we guarantee slashComparator(a,b)==0 iff a.equals(b).
* <br> (1) First of all, the builtin names "extend" and "in" are sorted ahead of other names
* <br> (2) Else, if one string starts with "this/", then it is considered smaller
* <br> (3) Else, if one string has fewer '/' than the other, then it is considered smaller.
* <br> (4) Else, we compare them lexically without case-sensitivity.
* <br> (5) Finally, we compare them lexically with case-sensitivity.
*/
public static final Comparator<String> slashComparator = new Comparator<String>() {
public final int compare(String a, String b) {
if (a==null) return (b==null)?0:-1; else if (b==null) return 1; else if (a.equals(b)) return 0;
if (a.equals("extends")) return -1; else if (b.equals("extends")) return 1;
if (a.equals("in")) return -1; else if (b.equals("in")) return 1;
if (a.startsWith("this/")) {
if (!b.startsWith("this/")) return -1;
} else if (b.startsWith("this/")) {
return 1;
}
int acount=0, bcount=0;
for(int i=0; i<a.length(); i++) { if (a.charAt(i)=='/') acount++; }
for(int i=0; i<b.length(); i++) { if (b.charAt(i)=='/') bcount++; }
if (acount!=bcount) return (acount<bcount) ? -1 : 1;
int result = a.compareToIgnoreCase(b);
return result!=0 ? result : a.compareTo(b);
}
};
/** Copy the given file from JAR into the destination file; if the destination file exists, we then do nothing.
* Returns true iff a file was created and written.
*/
private static boolean copy(String sourcename, String destname) {
File destfileobj = new File(destname);
if (destfileobj.isFile() && destfileobj.length()>0) return false;
boolean result = true;
InputStream in = null;
FileOutputStream out = null;
try {
in = Util.class.getClassLoader().getResourceAsStream(sourcename);
if (in==null) return false; // This means the file is not relevant for this setup, so we don't pop up a fatal dialog
out = new FileOutputStream(destname);
byte[] b = new byte[16384];
while(true) {
int numRead = in.read(b);
if (numRead < 0) break;
if (numRead > 0) out.write(b, 0, numRead);
}
} catch (IOException e) {
result=false;
}
if (!close(out)) result=false;
if (!close(in)) result=false;
if (!result) OurDialog.fatal("Error occurred in creating the file \""+destname+"\"");
return true;
}
/** Copy the list of files from JAR into the destination directory,
* then set the correct permissions on them if possible.
*
* @param executable - if true, we will attempt to set the file's "executable" permission (failure to do this is ignored)
* @param keepPath - if true, the full path will be created for the destination file
* @param destdir - the destination directory
* @param names - the files to copy from the JAR
*/
public static void copy(boolean executable, boolean keepPath, String destdir, String... names) {
String[] args = new String[names.length+2];
args[0] = "/bin/chmod"; // This does not work on Windows, but the "executable" bit is not needed on Windows anyway.
args[1] = (executable ? "700" : "600"); // 700 means read+write+executable; 600 means read+write.
int j=2;
for(int i=0; i<names.length; i++) {
String name = names[i];
String destname = name;
if (!keepPath) { int ii=destname.lastIndexOf('/'); if (ii>=0) destname=destname.substring(ii+1); }
destname=(destdir+'/'+destname).replace('/', File.separatorChar);
int last=destname.lastIndexOf(File.separatorChar);
new File(destname.substring(0,last+1)).mkdirs(); // Error will be caught later by the file copy
if (copy(name, destname)) { args[j]=destname; j++; }
}
if (onWindows() || j<=2) return;
String[] realargs = new String[j];
for(int i=0; i<j; i++) realargs[i] = args[i];
try {
Runtime.getRuntime().exec(realargs).waitFor();
} catch (Throwable ex) {
// We only intend to make a best effort
}
}
/** Copy file.content[from...f.length-1] into file.content[to...], then truncate the file after that point.
* <p> If (from > to), this means we simply delete the portion of the file beginning at "to" and up to but excluding "from".
* <p> If (from < to), this means we insert (to-from) number of ARBITRARY bytes into the "from" location
* and shift the original file content accordingly.
* <p> Note: after this operation, the file's current position will be moved to the start of the file.
* @throws IOException if (from < 0) || (to < 0) || (from >= file.length())
*/
public static void shift (RandomAccessFile file, long from, long to) throws IOException {
long total = file.length();
if (from<0 || from>=total || to<0) throw new IOException(); else if (from==to) {file.seek(0); return;}
final byte buf[] = new byte[4096];
int res;
if (from>to) {
while(true) {
file.seek(from);
if ((res=file.read(buf)) <= 0) { file.setLength(to); file.seek(0); return; }
file.seek(to); file.write(buf, 0, res); from=from+res; to=to+res;
}
} else {
file.seek(total);
for(long todo=to-from; todo>0;) {
if (todo >= buf.length) {file.write(buf); todo = todo - buf.length;} else {file.write(buf, 0, (int)todo); break;}
}
for(long todo=total-from; todo>0; total=total-res, todo=todo-res) {
if (todo > buf.length) res=buf.length; else res=(int)todo;
file.seek(total - res);
for(int done=0; done<res;) { int r=file.read(buf, done, res-done); if (r<=0) throw new IOException(); else done += r; }
file.seek(total - res + (to - from));
file.write(buf, 0, res);
}
}
file.seek(0);
}
/** Write a String into a PrintWriter, and encode special characters using XML-specific encoding.
*
* <p>
* In particular, it changes LESS THAN, GREATER THAN, AMPERSAND, SINGLE QUOTE, and DOUBLE QUOTE
* into "&lt;" "&gt;" "&amp;" "&apos;" and "&quot;" and turns any characters
* outside of the 32..126 range into the "&#xHHHH;" encoding
* (where HHHH is the 4 digit lowercase hexadecimal representation of the character value).
*
* @param out - the PrintWriter to write into
* @param str - the String to write out
*/
public static void encodeXML(PrintWriter out, String str) {
int n=str.length();
for(int i=0; i<n; i++) {
char c=str.charAt(i);
if (c=='<') { out.write("<"); continue; }
if (c=='>') { out.write(">"); continue; }
if (c=='&') { out.write("&"); continue; }
if (c=='\'') { out.write("'"); continue; }
if (c=='\"') { out.write("""); continue; }
if (c>=32 && c<=126) { out.write(c); continue; }
out.write("&#x");
String v=Integer.toString(c, 16);
for(int j=v.length(); j<4; j++) out.write('0');
out.write(v);
out.write(';');
}
}
/** Write a String into a StringBuilder, and encode special characters using XML-specific encoding.
*
* <p>
* In particular, it changes LESS THAN, GREATER THAN, AMPERSAND, SINGLE QUOTE, and DOUBLE QUOTE
* into "&lt;" "&gt;" "&amp;" "&apos;" and "&quot;" and turns any characters
* outside of the 32..126 range into the "&#xHHHH;" encoding
* (where HHHH is the 4 digit lowercase hexadecimal representation of the character value).
*
* @param out - the StringBuilder to write into
* @param str - the String to write out
*/
public static void encodeXML(StringBuilder out, String str) {
int n=str.length();
for(int i=0; i<n; i++) {
char c=str.charAt(i);
if (c=='<') { out.append("<"); continue; }
if (c=='>') { out.append(">"); continue; }
if (c=='&') { out.append("&"); continue; }
if (c=='\'') { out.append("'"); continue; }
if (c=='\"') { out.append("""); continue; }
if (c>=32 && c<=126) { out.append(c); continue; }
out.append("&#x");
String v=Integer.toString(c, 16);
for(int j=v.length(); j<4; j++) out.append('0');
out.append(v).append(';');
}
}
/** Encode special characters of a String using XML/HTML encoding.
*
* <p>
* In particular, it changes LESS THAN, GREATER THAN, AMPERSAND, SINGLE QUOTE, and DOUBLE QUOTE
* into "&lt;" "&gt;" "&amp;" "&apos;" and "&quot;" and turns any characters
* outside of the 32..126 range into the "&#xHHHH;" encoding
* (where HHHH is the 4 digit lowercase hexadecimal representation of the character value).
*/
public static String encode(String str) {
if (str.length() == 0) return str;
StringBuilder sb = new StringBuilder();
encodeXML(sb, str);
return sb.toString();
}
/** Write a list of Strings into a PrintWriter, where strs[2n] are written as-is, and strs[2n+1] are XML-encoded.
*
* <p> For example, if you call encodeXML(out, A, B, C, D, E), it is equivalent to the following:
* <br> out.print(A);
* <br> encodeXML(out, B);
* <br> out.print(C);
* <br> encodeXML(out, D);
* <br> out.print(E);
* <br> In other words, it writes the even entries as-is, and writes the odd entries using XML encoding.
*
* @param out - the PrintWriter to write into
* @param strs - the list of Strings to write out
*/
public static void encodeXMLs(PrintWriter out, String... strs) {
for(int i=0; i<strs.length; i++) {
if ((i%2)==0) out.print(strs[i]); else encodeXML(out,strs[i]);
}
}
/** Write a list of Strings into a StringBuilder, where strs[2n] are written as-is, and strs[2n+1] are XML-encoded.
*
* <p> For example, if you call encodeXML(out, A, B, C, D, E), it is equivalent to the following:
* <br> out.append(A);
* <br> encodeXML(out, B);
* <br> out.append(C);
* <br> encodeXML(out, D);
* <br> out.append(E);
* <br> In other words, it writes the even entries as-is, and writes the odd entries using XML encoding.
*
* @param out - the StringBuilder to write into
* @param strs - the list of Strings to write out
*/
public static void encodeXMLs(StringBuilder out, String... strs) {
for(int i=0; i<strs.length; i++) {
if ((i%2)==0) out.append(strs[i]); else encodeXML(out,strs[i]);
}
}
/** Finds the first occurrence of <b>small</b> within <b>big</b>.
* @param big - the String that we want to perform the search on
* @param small - the pattern we are looking forward
* @param start - the offset within "big" to start (for example: 0 means to start from the beginning of "big")
* @param forward - true if the search should go forward; false if it should go backwards
* @param caseSensitive - true if the search should be done in a case-sensitive manner
*
* @return 0 or greater if found, -1 if not found (Note: if small=="", then we always return -1)
*/
public static int indexOf(String big, String small, int start, boolean forward, boolean caseSensitive) {
int len=big.length(), slen=small.length();
if (slen==0) return -1;
while(start>=0 && start<len) {
for(int i=0 ; ; i++) {
if (i>=slen) return start;
if (start+i>=len) break;
int b=big.charAt(start+i), s=small.charAt(i);
if (!caseSensitive && b>='A' && b<='Z') b=(b-'A')+'a';
if (!caseSensitive && s>='A' && s<='Z') s=(s-'A')+'a';
if (b!=s) break;
}
if (forward) start++; else start--;
}
return -1;
}
/** Returns true iff running on Windows **/
public static boolean onWindows() {
return System.getProperty("os.name").toLowerCase(Locale.US).startsWith("windows");
};
/** Returns true iff running on Mac OS X. **/
public static boolean onMac() {
return System.getProperty("mrj.version")!=null || System.getProperty("os.name").toLowerCase(Locale.US).startsWith("mac ");
}
/** Returns the substring after the last "/" */
public static String tail(String string) { int i=string.lastIndexOf('/'); return (i<0) ? string : string.substring(i+1); }
}