001package daikon.split;
002
003import daikon.Global;
004import daikon.PptTopLevel;
005import java.io.BufferedWriter;
006import java.io.File;
007import java.io.FileNotFoundException;
008import java.io.IOException;
009import java.util.ArrayList;
010import java.util.List;
011import java.util.logging.Logger;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014import jtb.ParseException;
015import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
016import org.checkerframework.checker.nullness.qual.NonNull;
017import org.checkerframework.checker.nullness.qual.RequiresNonNull;
018import org.checkerframework.checker.regex.qual.Regex;
019import org.checkerframework.checker.signature.qual.BinaryName;
020import org.plumelib.util.FilesPlume;
021
022/**
023 * This class contains static methods {@link #parse_spinfofile(File)} which creates Splitterss from
024 * a {@code .spinfo} file, and {@link #load_splitters} which loads the splitters for a given Ppt.
025 */
026public class SplitterFactory {
027  private SplitterFactory() {
028    throw new Error("do not instantiate");
029  }
030
031  public static final Logger debug = Logger.getLogger("daikon.split.SplitterFactory");
032
033  /** The directory in which the Java files for the splitter will be made. */
034  // This must *not* be set in a static block, which happens before the
035  // Configuration object has had a chance to possibly set
036  // dkconfig_delete_splitters_on_exit.
037  private static @MonotonicNonNull String tempdir;
038
039  /**
040   * Boolean. If true, the temporary Splitter files are deleted on exit. Set it to "false" if you
041   * are debugging splitters.
042   */
043  public static boolean dkconfig_delete_splitters_on_exit = true;
044
045  /**
046   * String. Specifies which Java compiler is used to compile Splitters. This can be the full path
047   * name or whatever is used on the command line. Uses the current classpath.
048   */
049  public static String dkconfig_compiler
050      // "-source 8 -target 8" is a hack for when using a Java 9+ compiler but
051      // a Java 8 runtime.  A better solution would be to add
052      // these command-line arguments only when running
053      // SplitterFactoryTestUpdater, but that program does not support that.
054      = "javac -nowarn -source 8 -target 8 -classpath " + System.getProperty("java.class.path");
055
056  /**
057   * Positive integer. Specifies the Splitter compilation timeout, in seconds, after which the
058   * compilation process is terminated and retried, on the assumption that it has hung.
059   */
060  public static int dkconfig_compile_timeout = 20;
061
062  private static @MonotonicNonNull FileCompiler fileCompiler; // lazily initialized
063
064  /**
065   * guid is a counter that increments every time a file is written. It is used to ensure that every
066   * file written has a unique name.
067   */
068  private static int guid = 0;
069
070  /// Methods
071
072  /**
073   * Parses the Splitter info.
074   *
075   * @param infofile filename.spinfo
076   * @return a SpinfoFile encapsulating the parsed splitter info file
077   */
078  public static SpinfoFile parse_spinfofile(File infofile)
079      throws IOException, FileNotFoundException {
080    if (tempdir == null) {
081      tempdir = createTempDir();
082    }
083    if (!dkconfig_delete_splitters_on_exit) {
084      System.out.println("\rSplitters for this run created in " + tempdir);
085    }
086    return new SpinfoFile(infofile, tempdir);
087  }
088
089  /**
090   * Finds the splitters that apply to a given Ppt and loads them (that is, it populates
091   * SplitterList).
092   *
093   * @param ppt the Ppt
094   * @param spfiles a list of SpinfoFiles
095   */
096  @RequiresNonNull("tempdir")
097  public static void load_splitters(PptTopLevel ppt, List<SpinfoFile> spfiles) {
098    Global.debugSplit.fine("<<enter>> load_splitters");
099
100    for (SpinfoFile spfile : spfiles) {
101      SplitterObject[][] splitterObjects = spfile.getSplitterObjects();
102      StatementReplacer statementReplacer = spfile.getReplacer();
103      for (int i = 0; i < splitterObjects.length; i++) {
104        int numsplitters = splitterObjects[i].length;
105        if (numsplitters != 0) {
106          String ppt_name = splitterObjects[i][0].getPptName();
107          Global.debugSplit.fine(
108              "          load_splitters: "
109                  + ppt_name
110                  + ", "
111                  + ppt
112                  + "; match="
113                  + matchPpt(ppt_name, ppt));
114          if (matchPpt(ppt_name, ppt)) {
115            int numGood = 0;
116            // Writes, compiles, and loads the splitter .java files.
117            loadSplitters(splitterObjects[i], ppt, statementReplacer);
118            List<Splitter> sp = new ArrayList<>();
119            for (int k = 0; k < numsplitters; k++) {
120              if (splitterObjects[i][k].splitterExists()) {
121                @SuppressWarnings("nullness") // dependent: because splitterExists() = true
122                @NonNull Splitter splitter = splitterObjects[i][k].getSplitter();
123                sp.add(splitter);
124                numGood++;
125              } else {
126                // UNDONE: We should only output the load error if the
127                // compile was successful.
128                System.out.println(splitterObjects[i][k].getError());
129              }
130            }
131            System.out.printf(
132                "%s: %d of %d splitters successful%n", ppt_name, numGood, numsplitters);
133            if (sp.size() >= 1) {
134              SplitterList.put(ppt_name, sp.toArray(new Splitter[0]));
135            }
136            // delete this entry in the splitter array to prevent it from
137            // matching any other Ppts, since the documented behavior is that
138            // it only matches one.
139            splitterObjects[i] = new SplitterObject[0];
140          }
141        }
142      }
143    }
144    Global.debugSplit.fine("<<exit>>  load_splitters");
145  }
146
147  // Accessible for the purpose of testing.
148  public static String getTempDir() {
149    if (tempdir == null) {
150      tempdir = createTempDir();
151    }
152    return tempdir;
153  }
154
155  /**
156   * Writes, compiles, and loads the splitter {@code .java} files for each splitterObject in
157   * splitterObjects.
158   *
159   * @param splitterObjects are the splitterObjects for ppt
160   * @param ppt the Ppt for these splitterObjects
161   * @param statementReplacer a StatementReplacer for the replace statements to be used in these
162   *     splitterObjects
163   */
164  @RequiresNonNull("tempdir")
165  private static void loadSplitters(
166      SplitterObject[] splitterObjects, PptTopLevel ppt, StatementReplacer statementReplacer) {
167    Global.debugSplit.fine("<<enter>> loadSplitters - count: " + splitterObjects.length);
168
169    // System.out.println("loadSplitters for " + ppt.name);
170    if (splitterObjects.length == 0) {
171      return;
172    }
173    for (int i = 0; i < splitterObjects.length; i++) {
174      SplitterObject splitObj = splitterObjects[i];
175      String fileName = getFileName(splitObj.getPptName());
176      StringBuilder fileContents;
177      try {
178        SplitterJavaSource splitterWriter =
179            new SplitterJavaSource(
180                splitObj, splitObj.getPptName(), fileName, ppt.var_infos, statementReplacer);
181        fileContents = splitterWriter.getFileText();
182      } catch (ParseException e) {
183        System.out.println("Error in SplitterFactory while writing splitter java file for: ");
184        System.out.println(splitObj.condition() + " cannot be parsed.");
185        continue;
186      }
187      String fileAddress = tempdir + fileName;
188      @SuppressWarnings("signature") // safe, has been quoted
189      @BinaryName String fileName_bn = fileName;
190      splitObj.setClassName(fileName_bn);
191      try (BufferedWriter writer = FilesPlume.newBufferedFileWriter(fileAddress + ".java")) {
192        if (dkconfig_delete_splitters_on_exit) {
193          new File(fileAddress + ".java").deleteOnExit();
194          new File(fileAddress + ".class").deleteOnExit();
195        }
196        writer.write(fileContents.toString());
197        writer.flush();
198      } catch (IOException ioe) {
199        System.out.println("Error while writing Splitter file: " + fileAddress);
200        debug.fine(ioe.toString());
201      }
202    }
203    List<String> fileNames = new ArrayList<>();
204    for (int i = 0; i < splitterObjects.length; i++) {
205      fileNames.add(splitterObjects[i].getFullSourcePath());
206    }
207    String errorOutput = null;
208    try {
209      errorOutput = compileFiles(fileNames);
210    } catch (IOException ioe) {
211      System.out.println("Error while compiling Splitter files (Daikon will continue):");
212      debug.fine(ioe.toString());
213    }
214    boolean errorOutputExists = errorOutput != null && !errorOutput.equals("");
215    if (errorOutputExists && !PptSplitter.dkconfig_suppressSplitterErrors) {
216      System.out.println();
217      System.out.println(
218          "Errors while compiling Splitter files (Daikon will use non-erroneous splitters):");
219      System.out.println(errorOutput);
220    }
221    for (int i = 0; i < splitterObjects.length; i++) {
222      splitterObjects[i].load();
223    }
224
225    Global.debugSplit.fine("<<exit>>  loadSplitters");
226  }
227
228  /**
229   * Compiles the files given by fileNames. Return the error output.
230   *
231   * @return the error output from compiling the files
232   * @param fileNames paths to the files to be compiled as Strings
233   * @throws IOException if there is a problem reading a file
234   */
235  private static String compileFiles(List<String> fileNames) throws IOException {
236    // We delay setting fileCompiler until now because we want to permit
237    // the user to set the dkconfig_compiler variable.  Note that our
238    // timeout is specified in seconds, but the parameter to FileCompiler
239    // is specified in milliseconds.
240    if (fileCompiler == null) {
241      fileCompiler = new FileCompiler(dkconfig_compiler, 1000 * (long) dkconfig_compile_timeout);
242    }
243    return fileCompiler.compileFiles(fileNames);
244  }
245
246  /** Determine whether a Ppt's name matches the given pattern. */
247  private static boolean matchPpt(String ppt_name, PptTopLevel ppt) {
248    if (ppt.name.equals(ppt_name)) {
249      return true;
250    }
251    if (ppt_name.endsWith(":::EXIT")) {
252      String regex = Pattern.quote(ppt_name) + "[0-9]+";
253      if (matchPptRegex(regex, ppt)) {
254        return true;
255      }
256    }
257
258    // Look for corresponding EXIT ppt. This is because the exit ppt usually has
259    // more relevant variables in scope (eg. return, hashcodes) than the enter.
260    String regex;
261    int index = ppt_name.indexOf("OBJECT");
262    if (index == -1) {
263      // Didn't find "OBJECT" suffix; add ".*EXIT".
264      regex = Pattern.quote(ppt_name) + ".*EXIT";
265    } else {
266      // Found "OBJECT" suffix.
267      if (ppt_name.length() > 6) {
268        regex = Pattern.quote(ppt_name.substring(0, index - 1)) + ":::OBJECT";
269      } else {
270        regex = Pattern.quote(ppt_name);
271      }
272    }
273    return matchPptRegex(regex, ppt);
274  }
275
276  private static boolean matchPptRegex(@Regex String ppt_regex, PptTopLevel ppt) {
277    // System.out.println("matchPptRegex: " + ppt_regex);
278    Pattern pattern = Pattern.compile(ppt_regex);
279    String name = ppt.name;
280    Matcher matcher = pattern.matcher(name);
281    // System.out.println("  considering " + name);
282    return matcher.find();
283  }
284
285  /**
286   * Returns a file name for a splitter file to be used with a Ppt with the name, ppt_name. The file
287   * name is ppt_name with all characters which are invalid for use in a java file name (such as
288   * ".") replaced with "_". Then "_guid" is append to the end. For example if ppt_name is
289   * "myPackage.myClass.someMethod" and guid = 12, then the following would be returned:
290   * "myPackage_myClass_someMethod_12".
291   *
292   * @param ppt_name the name of the Ppt that the splitter Java file wil be used with
293   */
294  private static String getFileName(String ppt_name) {
295    String splitterName = clean(ppt_name);
296    splitterName = splitterName + "_" + guid;
297    guid++;
298    return splitterName;
299  }
300
301  /**
302   * Cleans str by replacing all characters that are not valid java indentifier parts with "_".
303   *
304   * @param str the string to be cleaned
305   * @return str with all non-Java-indentifier parts replaced with "_"
306   */
307  private static String clean(String str) {
308    char[] cleaned = str.toCharArray();
309    for (int i = 0; i < cleaned.length; i++) {
310      char c = cleaned[i];
311      if (!Character.isJavaIdentifierPart(c)) {
312        cleaned[i] = '_';
313      }
314    }
315    return new String(cleaned);
316  }
317
318  /**
319   * Creates the temporary directory in which splitter files will be stored. The return value
320   * includes a trailing file separtor (e.g., "/"), unless the return value is "".
321   *
322   * @return the name of the temporary directory. This is where the Splitters are created.
323   */
324  private static String createTempDir() {
325    try {
326      File tmpDir = FilesPlume.createTempDir("daikon", "split");
327      if (dkconfig_delete_splitters_on_exit) {
328        tmpDir.deleteOnExit();
329      }
330      return tmpDir.getPath() + File.separator;
331    } catch (IOException e) {
332      debug.fine(e.toString());
333    }
334    return ""; // Use current directory
335  }
336}