package de.peeeq.wurstscript.utils; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.*; import com.google.common.collect.ImmutableList.Builder; import com.google.common.io.Files; import de.peeeq.wurstio.Pjass; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.names.NameLink; import de.peeeq.wurstscript.attributes.prettyPrint.DefaultSpacer; import de.peeeq.wurstscript.jassIm.JassImElementWithName; import de.peeeq.wurstscript.parser.WPos; import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeUnknown; import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.NotNull; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.*; import java.time.Duration; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.function.*; import java.util.stream.Collector; import java.util.stream.Collectors; public class Utils { @SuppressWarnings("rawtypes") public static int size(Iterable i) { if (i instanceof Collection) { return ((Collection) i).size(); } int size = 0; for (@SuppressWarnings("unused") Object o : i) { size++; } return size; } public static void printIndent(StringBuilder sb, int indent) { for (int i = 0; i < indent; i++) { sb.append("\t"); } } @SafeVarargs public static List list(T... args) { List result = new NotNullList<>(); Collections.addAll(result, args); return result; } public static List removedDuplicates(List list) { List result = new NotNullList<>(); for (T t : list) { if (!result.contains(t)) { result.add(t); } } return result; } public static void printSep(StringBuilder sb, String seperator, T[] args) { for (int i = 0; i < args.length; i++) { if (i > 0) { sb.append(seperator); } sb.append(args[i]); } } public static int parseInt(String yytext) { if (yytext.startsWith("0")) { return Integer.parseInt(yytext, 8); } else { return Integer.parseInt(yytext); } } public static int parseAsciiInt(String yytext) throws NumberFormatException { if (yytext.length() == 3) { // 'x' can directly return return yytext.charAt(1); } int result = 0; int i = 1; int chars = 0; for (; i < yytext.length() - 1; i++) { if (yytext.charAt(i) == '\\') { i++; } result = result * 256 + yytext.charAt(i); chars++; } if (chars != 1 && chars != 4) { throw new NumberFormatException("Ascii ints must have 4 or 1 characters but this one has " + chars + " characters."); } return result; } public static int parseOctalInt(String yytext) { return (int) Long.parseLong(yytext, 8); } public static int parseHexInt(String yytext, int offset) { if (yytext.startsWith("-")) { return (int) -Long.parseLong(yytext.substring(offset+1), 16); } else { return (int) Long.parseLong(yytext.substring(offset), 16); } } public static String printSep(String sep, T[] args) { StringBuilder sb = new StringBuilder(); printSep(sb, sep, args); return sb.toString(); } public static String printSep(String sep, List args) { return args.stream().map(String::valueOf).collect(Collectors.joining(sep)); } /** * is a piece of code jass code? */ public static boolean isJassCode(@Nullable Element pos) { while (pos != null) { if (pos instanceof WPackage) { return false; // code is inside package -> wurstscript code } pos = pos.getParent(); } return true; // no package found -> jass code } public static String join(Iterable hints, String seperator) { StringBuilder builder = new StringBuilder(); boolean first = true; for (T s : hints) { if (!first) { builder.append(seperator); } builder.append(s); first = false; } return builder.toString(); } public static String join(T[] arguments, String seperator) { StringBuilder builder = new StringBuilder(); boolean first = true; for (T s : arguments) { if (!first) { builder.append(seperator); } builder.append(s); first = false; } return builder.toString(); } /** * sorts a list with partitial ordering topologically. If a > b then a will * appear before b in the result list * * @param items items to sort * @param biggerItems a function to get all the bigger items for a given item * @return a sorted list * @throws TopsortCycleException if there exist items a,b so that a > b and b > a */ public static List topSort(Collection items, java.util.function.Function> biggerItems) throws TopsortCycleException { Set visitedItems = new HashSet<>(); List result = new ArrayList<>(items.size()); LinkedList activeItems = Lists.newLinkedList(); for (T t : items) { if (t == null) throw new IllegalArgumentException(); topSortHelper(result, visitedItems, activeItems, biggerItems::apply, t); } return result; } private static void topSortHelper(List result, Set visitedItems, LinkedList activeItems, Function> biggerItems, T item) throws TopsortCycleException { if (visitedItems.contains(item)) { return; } if (activeItems.contains(item)) { // This is not constant time, could be // more efficient while (activeItems.get(0) != item) { activeItems.remove(0); } throw new TopsortCycleException(activeItems); } activeItems.add(item); visitedItems.add(item); for (T t : biggerItems.apply(item)) { if (t == null) throw new IllegalArgumentException(); topSortHelper(result, visitedItems, activeItems, biggerItems, t); } result.add(item); activeItems.removeLast(); } @SafeVarargs public static boolean oneOf(T obj, T... ts) { for (T t : ts) { if (t.equals(obj)) { return true; } } return false; } public static T getFirst(Iterable ts) { for (T t : ts) { return t; } throw new Error("collection has no first element"); } public static T getLast(List ts) { return ts.get(ts.size() - 1); } public static String printElement(Optional el) { return el.map(e -> { String type = makeReadableTypeName(e); String name = ""; if (e instanceof ExprFunctionCall) { ExprFunctionCall fc = (ExprFunctionCall) e; return "function call " + fc.getFuncName() + "()"; } else if (e instanceof FuncDef) { FuncDef fd = (FuncDef) e; return "function " + fd.getName(); } else if (e instanceof OnDestroyDef) { return "destroy function for " + e.attrNearestStructureDef().getName(); } else if (e instanceof ConstructorDef) { return "constructor for " + e.attrNearestStructureDef().getName(); } else if (e instanceof LocalVarDef) { LocalVarDef l = (LocalVarDef) e; return "local variable " + l.getName(); } else if (e instanceof VarDef) { VarDef l = (VarDef) e; return "variable " + l.getName(); } else if (e instanceof AstElementWithNameId) { name = ((AstElementWithNameId) e).getNameId().getName(); } else if (e instanceof WImport) { WImport wImport = (WImport) e; return "import " + wImport.getPackagename(); } else if (e instanceof TypeExprSimple) { TypeExprSimple t = (TypeExprSimple) e; name = t.getTypeName(); if (t.getTypeArgs().size() > 0) { name += "{"; boolean first = true; StringBuilder builder = new StringBuilder(); builder.append(name); for (TypeExpr ta : t.getTypeArgs()) { if (!first) { builder.append(", "); } builder.append(printElement(ta)); first = false; } name = builder.toString(); name += "}"; } type = "type"; } return type + " " + name; }).orElse("null"); } public static String printElement(@Nullable Element e) { return printElement(Optional.ofNullable(e)); } private static String makeReadableTypeName(Element e) { String type = e.getClass().getName() .replaceAll("de.peeeq.wurstscript.ast.", "") .replaceAll("Impl$", "").replaceAll("Def$", "").toLowerCase(); if (type.equals("wpackage")) { type = "package"; } return type; } public static int inBorders(int min, int x, int max) { return Math.max(min, Math.min(max, x)); } public static String printStackTrace(StackTraceElement[] stackTrace) { StringBuilder builder = new StringBuilder(); for (StackTraceElement s : stackTrace) { builder.append(s.toString()); builder.append("\n"); } return builder.toString(); } public static String printExceptionWithStackTrace(Throwable t) { StringBuilder builder = new StringBuilder(); builder.append(t); builder.append("\n"); for (; ; ) { for (StackTraceElement s : t.getStackTrace()) { builder.append(s.toString()); builder.append("\n"); } t = t.getCause(); if (t == null) { break; } builder.append("Caused by: ").append(t).append("\n"); } return builder.toString(); } public static Optional getAstElementAtPos(Element elem, int caretPosition, boolean usesMouse) { List betterResults = Lists.newArrayList(); for (int i = 0; i < elem.size(); i++) { getAstElementAtPos(elem.get(i), caretPosition, usesMouse).map(betterResults::add); } if (betterResults.size() == 0) { if (elem instanceof Identifier) { return Optional.ofNullable(elem.getParent()); } return Optional.ofNullable(elem); } else { return bestResult(betterResults); } } public static Optional getAstElementAtPos(Element elem, int line, int column, boolean usesMouse) { if (elem instanceof ModuleInstanciation) { // do not helicopter into module instantiations return Optional.of(elem); } List betterResults = Lists.newArrayList(); for (int i = 0; i < elem.size(); i++) { Element e = elem.get(i); if (elementContainsPos(e, line, column, usesMouse) || e.attrSource().isArtificial()) { getAstElementAtPos(e, line, column, usesMouse).map(betterResults::add); } } Optional bestResult = bestResult(betterResults); if (!bestResult.isPresent()) { if (elem instanceof Identifier) { return Optional.ofNullable(elem.getParent()); } return Optional.of(elem); } else { return bestResult; } } public static Optional getAstElementAtPosIgnoringLists(Element elem, int caretPosition, boolean usesMouse) { Optional r = getAstElementAtPos(elem, caretPosition, usesMouse); while (r.isPresent() && r.get() instanceof List) { r = r.flatMap(el -> Optional.ofNullable(el.getParent())); } return r; } /** * return the element with the smallest size */ private static Optional bestResult(List betterResults) { int minSize = Integer.MAX_VALUE; Optional min = Optional.empty(); for (Element e : betterResults) { WPos source = e.attrSource(); int size = source.isArtificial() ? Integer.MAX_VALUE : source.getRightPos() - source.getLeftPos(); if (size < minSize) { minSize = size; min = Optional.of(e); } } return min; } public static boolean elementContainsPos(Element e, int pos, boolean usesMouse) { return e.attrSource().getLeftPos() <= pos && e.attrSource().getRightPos() >= pos + (usesMouse ? 1 : 0); } private static boolean elementContainsPos(Element e, int line, int column, boolean usesMouse) { WPos pos = e.attrSource(); if (pos.getLine() > line) { return false; } if (pos.getEndLine() < line) { return false; } return (pos.getLine() < line || pos.getStartColumn() <= column) && (pos.getEndLine() > line || pos.getEndColumn() >= column); } public static List sortByName( Collection c) { List r = Lists.newArrayList(c); r.sort(Comparator.comparing(NameDef::getName)); return r; } public static String printPos(WPos source) { return source.getFile() + ", line " + source.getLine(); } public static boolean isEmptyCU(Optional cu) { return !cu.isPresent() || (cu.get().getJassDecls().size() + cu.get().getPackages().size() == 0); } public static String printElementWithSource(Optional e) { Optional src = e.map(Element::attrSource); return printElement(e) + " (" + src.map(WPos::printShort).orElse("unknown position") + ")"; } public static int[] copyArray(int[] ar) { int[] r = new int[ar.length]; System.arraycopy(ar, 0, r, 0, ar.length); return r; } public static String toFirstUpper(String s) { if (s.isEmpty()) { return s; } return s.substring(0, 1).toUpperCase() + s.substring(1); } public static Optional getParentVarDef(Optional node) { while (node.isPresent()) { if (node.get() instanceof VarDef) { return node.map(n -> (VarDef) n); } node = node.map(Element::getParent); } return null; } public static String printAlternatives(Collection alternatives) { List result = Lists.newArrayList(); for (NameLink a : alternatives) { WPos source = a.getDef().attrSource(); String s = Utils.printElement(a.getDef()) + " defined in line " + source.getLine() + " (" + source.getFile() + ")"; result.add(s); } return " * " + Utils.join(result, "\n * "); } public static Multimap inverse(Multimap orig) { Multimap result = LinkedHashMultimap.create(); for (Entry e : orig.entries()) { result.put(e.getValue(), e.getKey()); } return result; } public static , S> TreeMap> inverseMapToSet(Map orig) { TreeMap> result = new TreeMap<>(); for (Entry e : orig.entrySet()) { result.computeIfAbsent(e.getValue(), x -> new LinkedHashSet<>()).add(e.getKey()); } return result; } public static boolean isSubsequenceIgnoreCase(String a, String b) { int bPos = -1; for (int i = 0; i < a.length(); i++) { char c = Character.toLowerCase(a.charAt(i)); do { bPos++; if (bPos >= b.length()) { return false; } } while (Character.toLowerCase(b.charAt(bPos)) != c); } return true; } public static boolean isSubsequence(String a, String b) { int bPos = -1; for (int i = 0; i < a.length(); i++) { char c = a.charAt(i); do { bPos++; if (bPos >= b.length()) { return false; } } while (b.charAt(bPos) != c); } return true; } public static List subsequenceLengthes(String a, String b) { List subseqLength = Lists.newArrayList(); while (!a.isEmpty()) { int prefixlen = a.length(); while (prefixlen > 0 && !containsPrefix(b, a, prefixlen)) { prefixlen--; } if (prefixlen == 0) { subseqLength.add(0); break; } subseqLength.add(prefixlen); String found = a.substring(0, prefixlen); b = b.substring(prefixlen + b.indexOf(found)); a = a.substring(prefixlen); } return subseqLength; } /** * checks if b contains the first n characters of a as a substring */ private static boolean containsPrefix(String b, String a, int n) { return b.contains(a.substring(0, n)); } public static T getFirstAndOnly(Collection c) { if (c.size() != 1) { throw new Error("Size must be 1 but was " + c.size()); } return getFirst(c); } private static void escapeStringParts(String v, StringBuilder builder) { for (int i = 0; i < v.length(); i++) { char c = v.charAt(i); switch (c) { case '\n': builder.append("\\n"); break; case '\r': builder.append("\\r"); break; case '\"': builder.append("\\\""); break; case '\t': builder.append("\\t"); break; case '\\': builder.append("\\\\"); break; default: builder.append(c); } } } public static String escapeStringWithoutQuotes(String v) { StringBuilder builder = new StringBuilder(); escapeStringParts(v, builder); return builder.toString(); } public static String escapeString(String v) { StringBuilder builder = new StringBuilder(); builder.append("\""); escapeStringParts(v, builder); builder.append("\""); return builder.toString(); } public static String escapeHtml(String s) { s = s.replace("<", "<"); s = s.replace(">", ">"); return s; } /** * returns the filename from the given path */ public static String fileName(String path) { int pos = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (pos > 0) { return path.substring(pos + 1); } return path; } public static String printException(Throwable e) { return e + "\n" + Utils.printExceptionWithStackTrace(e); } public static String stripHtml(String s) { return s.replaceAll("<.*?>", ""); } public static Iterable iterateReverse(final List elements) { return () -> new Iterator() { final ListIterator it = elements.listIterator(elements.size()); @Override public boolean hasNext() { return it.hasPrevious(); } @Override public T next() { return it.previous(); } @Override public void remove() { it.remove(); } }; } public static String readWholeStream(BufferedReader r) throws IOException { StringBuilder sb = new StringBuilder(); Optional line; while ((line = Optional.ofNullable(r.readLine())).isPresent()) { line.map(sb::append); } return sb.toString(); } public static String readWholeStream(InputStream inputStream) throws IOException { return readWholeStream(new BufferedReader(new InputStreamReader(inputStream))); } @SuppressWarnings("unchecked") public static Optional getNearestByType(Optional e, Class clazz) { while (e.isPresent()) { if (clazz.isInstance(e.get())) { return Optional.of((T) e.get()); } e = e.flatMap(el -> Optional.ofNullable(el.getParent())); } return Optional.empty(); } public static Comparator compareByNameIm() { return Comparator.comparing(JassImElementWithName::getName); } public static String getParameterListText(AstElementWithParameters f) { StringBuilder descr = new StringBuilder(); for (WParameter p : f.getParameters()) { if (descr.length() > 0) { descr.append(", "); } descr.append(p.attrTyp()).append(" ").append(p.getName()); } return descr.toString(); } public static List subList(List l, int start) { return subList(l, start, l.size() - 1); } public static List subList(List l, int start, int stop) { List result = Lists.newArrayListWithCapacity(stop - start); for (int i = start; i <= stop; i++) { result.add(l.get(i)); } return result; } public static ImmutableMap mergeMaps( ImmutableMap a, ImmutableMap b, Function2 mergeFunc) { if (a.isEmpty()) return b; if (b.isEmpty()) return a; ImmutableMap.Builder builder = ImmutableMap.builder(); for (Entry e : a.entrySet()) { K key = e.getKey(); if (b.containsKey(key)) { builder.put(key, mergeFunc.apply(e.getValue(), b.get(key))); } else { builder.put(e); } } for (Entry e : b.entrySet()) { K key = e.getKey(); if (!a.containsKey(key)) { builder.put(e); } } return builder.build(); } public static ImmutableSetMultimap mergeMultiMaps( ImmutableSetMultimap a, ImmutableSetMultimap b) { if (a.isEmpty()) return b; if (b.isEmpty()) return a; ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); builder.putAll(a); builder.putAll(b); return builder.build(); } public static ImmutableSet mergeSets(ImmutableSet a, ImmutableSet b) { ImmutableSet.Builder builder = ImmutableSet.builder(); builder.addAll(a).addAll(b); return builder.build(); } public static T[] joinArrays(T[] a, T[] b) { T[] res = Arrays.copyOf(a, a.length + b.length); System.arraycopy(b, 0, res, a.length, b.length); return res; } public static void removeValuesFromMap(Map map, Collection removed) { map.entrySet().removeIf(e -> removed.contains(e.getValue())); } public static ImmutableList emptyList() { return ImmutableList.of(); } public static Collector> toImmutableList() { return new Collector, ImmutableList>() { @Override public Supplier> supplier() { return ImmutableList::builder; } @Override public BiConsumer, T> accumulator() { return Builder::add; } @Override public BinaryOperator> combiner() { return (a, b) -> a.addAll(b.build()); } @Override public java.util.function.Function, ImmutableList> finisher() { return Builder::build; } @Override public Set characteristics() { return Collections.emptySet(); } }; } public static MouseListener onClickDo(Consumer onclick) { return new MouseAdapter() { @Override public void mouseClicked(@Nullable MouseEvent e) { Preconditions.checkNotNull(e); onclick.accept(e); } }; } public static boolean isWurstFile(File f) { return isWurstFile(f.getName()); } public static boolean isWurstFile(String fileName) { return fileName.endsWith(".wurst") || fileName.endsWith(".jurst"); } public static String getLibName(File f) { return f.getName().replaceAll("\\.[jw]urst$", ""); } public static String repeat(char c, int size) { StringBuilder result = new StringBuilder(size); for (int i = 0; i < size; i++) { result.append(c); } return result.toString(); } public static boolean elementContained(Optional e, Element in) { while (e.isPresent()) { if (e.get() == in) { return true; } e = e.flatMap(el -> Optional.ofNullable(el.getParent())); } return false; } /** * Executes a shell command in a given folder and returns the output of the executed command */ public static String exec(File folder, String... cmds) { try { Process p = new ProcessBuilder(cmds) .directory(folder) .start(); int res = p.waitFor(); if (res != 0) { throw new RuntimeException("Could not execute " + Utils.join(Arrays.asList(cmds), " ") + "\nErrors:\n" + convertStreamToString(p.getErrorStream()) + "\nOutput:\n" + convertStreamToString(p.getInputStream())); } return convertStreamToString(p.getInputStream()); } catch (Exception e) { throw new RuntimeException(e); } } /** * Converts an input stream to a string *

* see http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string */ public static String convertStreamToString(InputStream is) { try(Scanner s = new Scanner(is).useDelimiter("\\A")) { return s.hasNext() ? s.next() : ""; } } public static byte[] convertStreamToBytes(InputStream is) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int nRead; byte[] data = new byte[16384]; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); return buffer.toByteArray(); } /** * join lines */ public static String string(String... lines) { return String.join("\n", lines) + "\n"; } /** * Extracts a resource from the jar, stores it in a temp file and returns the abolute path to the tempfile */ public static synchronized String getResourceFile(String name) { return getResourceFileF(name).getAbsolutePath(); } /** * Extracts a resource from the jar, stores it in a temp file and returns the abolute path to the tempfile */ public static synchronized File getResourceFileF(String name) { try { Optional f = Optional.ofNullable(resourceMap.get(name)); if (f.isPresent() && f.get().exists()) { return f.get(); } String[] parts = splitFilename(name); File fi = File.createTempFile(parts[0], parts[1]); fi.deleteOnExit(); try (InputStream is = Pjass.class.getClassLoader().getResourceAsStream(name)) { if (is == null) { throw new RuntimeException("Could not find resource file " + name); } byte[] bytes = Utils.convertStreamToBytes(is); Files.write(bytes, fi); resourceMap.put(name, fi); return fi; } } catch (IOException e) { throw new RuntimeException(e); } } @NotNull private static String[] splitFilename(String name) { int dotPos = name.lastIndexOf('.'); if (dotPos >= 0) { return new String[] { name.substring(0, dotPos), name.substring(dotPos + 1) }; } return new String[] { name, "" }; } private static final Map resourceMap = new HashMap<>(); public static String elementNameWithPath(AstElementWithNameId n) { StringBuilder result = new StringBuilder(n.getNameId().getName()); Optional e = Optional.ofNullable(n.getParent()); while (e.isPresent()) { if (e.get() instanceof AstElementWithNameId) { result.insert(0, ((AstElementWithNameId) e.get()).getNameId().getName() + "_"); } e = e.flatMap(el -> Optional.ofNullable(el.getParent())); } return result.toString(); } @SafeVarargs public static ImmutableList append(List list, T ... elems) { Builder builder = ImmutableList.builderWithExpectedSize(list.size() + elems.length); builder.addAll(list); for (T elem : elems) { builder.add(elem); } return builder.build(); } @SafeVarargs public static ImmutableList concatLists(List ...lists) { Builder builder = ImmutableList.builder(); for (List list : lists) { builder.addAll(list); } return builder.build(); } public static String prettyPrint(Element e) { StringBuilder sb = new StringBuilder(); e.prettyPrint(new DefaultSpacer(), sb, 0); return sb.toString(); } public static String prettyPrintWithLine(Element e) { StringBuilder sb = new StringBuilder(); sb.append(e.attrSource().getFile()).append(":").append(e.attrSource().getLine()).append(": "); e.prettyPrint(new DefaultSpacer(), sb, 4); return sb.toString(); } public static String printTypeExpr(TypeExpr t) { WurstType wt = t.attrTyp(); if (wt instanceof WurstTypeUnknown) { if (t instanceof TypeExprSimple) { return ((TypeExprSimple) t).getTypeName(); } return "???"; } return wt.toString(); } /** * Copy of the list without its last element */ public static List init(List list) { return list.stream().limit(list.size() - 1).collect(Collectors.toList()); } public static Optional getEnvOrConfig(String varName) { String res = System.getenv(varName); if (res == null || res.isEmpty()) { res = System.getProperty(varName); } if (res == null || res.isEmpty()) { return Optional.empty(); } return Optional.of(res); } public static class ExecResult { private final String stdOut; private final String stdErr; public ExecResult(String stdOut, String stdErr) { this.stdOut = stdOut; this.stdErr = stdErr; } public String getStdOut() { return stdOut; } public String getStdErr() { return stdErr; } } public static ExecResult exec(ProcessBuilder pb, Duration duration, Consumer onInput) throws IOException, InterruptedException { Process process = pb.start(); class Collector extends Thread { private final StringBuilder sb = new StringBuilder(); private final InputStream in; Collector(InputStream in) { this.in = in; } @Override public void run() { try (BufferedReader input = new BufferedReader(new InputStreamReader(in))) { Optional line; while ((line = Optional.ofNullable(input.readLine())).isPresent()) { onInput.accept(line.get()); sb.append(line.get()).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } } String getContents() { return sb.toString(); } } Collector cIn = new Collector(process.getInputStream()); cIn.start(); Collector cErr = new Collector(process.getErrorStream()); cErr.start(); boolean r = process.waitFor(duration.toMillis(), TimeUnit.MILLISECONDS); process.destroyForcibly(); cIn.join(); cErr.join(); if (!r) { throw new IOException("Timeout running external tool"); } if (process.exitValue() != 0) { throw new IOException("Failure when running external tool"); } return new ExecResult(cIn.getContents(), cErr.getContents()); } public static String makeUniqueName(String baseName, Predicate isValid) { if (isValid.test(baseName)) { return baseName; } int minI = 1; int maxI = 1; while (true) { String name = baseName + "_" + maxI; if (isValid.test(name)) { break; } minI = maxI; maxI *= 2; } while (minI < maxI) { int mid = minI + (maxI - minI) / 2; String name = baseName + "_" + mid; if (isValid.test(name)) { maxI = mid; } else { minI = mid + 1; } } return baseName + "_" + maxI; } }