jps: CompiledClass accepts multiple source files
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / instrumentation / RmiStubsGenerator.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
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 package org.jetbrains.jps.incremental.instrumentation;
17
18 import com.intellij.compiler.instrumentation.InstrumentationClassFinder;
19 import com.intellij.execution.process.BaseOSProcessHandler;
20 import com.intellij.execution.process.ProcessAdapter;
21 import com.intellij.execution.process.ProcessEvent;
22 import com.intellij.execution.process.ProcessOutputTypes;
23 import com.intellij.openapi.util.Key;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.util.ArrayUtil;
27 import com.intellij.util.SmartList;
28 import com.intellij.util.SystemProperties;
29 import gnu.trove.THashMap;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32 import org.jetbrains.jps.ModuleChunk;
33 import org.jetbrains.jps.ProjectPaths;
34 import org.jetbrains.jps.incremental.*;
35 import org.jetbrains.jps.incremental.messages.BuildMessage;
36 import org.jetbrains.jps.incremental.messages.CompilerMessage;
37 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
38 import org.jetbrains.jps.model.java.JpsJavaSdkType;
39 import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerConfiguration;
40 import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions;
41 import org.jetbrains.jps.model.java.compiler.RmicCompilerOptions;
42 import org.jetbrains.jps.model.library.sdk.JpsSdk;
43 import org.jetbrains.jps.service.SharedThreadPool;
44
45 import java.io.File;
46 import java.io.IOException;
47 import java.rmi.Remote;
48 import java.util.*;
49 import java.util.concurrent.Future;
50
51 /**
52  * @author Eugene Zhuravlev
53  *         Date: 11/30/12
54  */
55 public class RmiStubsGenerator extends ClassProcessingBuilder {
56   private static final String REMOTE_INTERFACE_NAME = Remote.class.getName().replace('.', '/');
57   private static final File[] EMPTY_FILE_ARRAY = new File[0];
58   private static Key<Boolean> IS_ENABLED = Key.create("_rmic_compiler_enabled_");
59
60   public RmiStubsGenerator() {
61     super(BuilderCategory.CLASS_INSTRUMENTER);
62   }
63
64   @Override
65   protected String getProgressMessage() {
66     return "Generating RMI stubs...";
67   }
68
69   @NotNull
70   @Override
71   public String getPresentableName() {
72     return "rmic";
73   }
74
75   @Override
76   public void buildStarted(CompileContext context) {
77     super.buildStarted(context);
78     final RmicCompilerOptions rmicOptions = getOptions(context);
79     IS_ENABLED.set(context, rmicOptions != null && rmicOptions.IS_EANABLED);
80   }
81
82   @Override
83   protected boolean isEnabled(CompileContext context, ModuleChunk chunk) {
84     return IS_ENABLED.get(context, Boolean.FALSE);
85   }
86
87   @Override
88   protected ExitCode performBuild(CompileContext context, ModuleChunk chunk, InstrumentationClassFinder finder, OutputConsumer outputConsumer) {
89     ExitCode exitCode = ExitCode.NOTHING_DONE;
90     if (!outputConsumer.getCompiledClasses().isEmpty()) {
91       final Map<ModuleBuildTarget, Collection<ClassItem>> remoteClasses = new THashMap<ModuleBuildTarget, Collection<ClassItem>>();
92       for (ModuleBuildTarget target : chunk.getTargets()) {
93         for (CompiledClass compiledClass : outputConsumer.getTargetCompiledClasses(target)) {
94           try {
95             if (isRemote(compiledClass, finder)) {
96               Collection<ClassItem> list = remoteClasses.get(target);
97               if (list ==  null) {
98                 list = new ArrayList<ClassItem>();
99                 remoteClasses.put(target, list);
100               }
101               list.add(new ClassItem(compiledClass));
102             }
103           }
104           catch (IOException e) {
105             context.processMessage(new CompilerMessage(getPresentableName(), e));
106           }
107         }
108       }
109       if (!remoteClasses.isEmpty()) {
110         exitCode = generateRmiStubs(context, remoteClasses, chunk, outputConsumer);
111       }
112     }
113     return exitCode;
114   }
115
116   private ExitCode generateRmiStubs(final CompileContext context,
117                                     Map<ModuleBuildTarget, Collection<ClassItem>> remoteClasses,
118                                     ModuleChunk chunk,
119                                     OutputConsumer outputConsumer) {
120     ExitCode exitCode = ExitCode.NOTHING_DONE;
121
122     final Collection<File> classpath = ProjectPaths.getCompilationClasspath(chunk, false);
123     final StringBuilder buf = new StringBuilder();
124     for (File file : classpath) {
125       if (buf.length() > 0) {
126         buf.append(File.pathSeparator);
127       }
128       buf.append(file.getPath());
129     }
130     final String classpathString = buf.toString();
131     final String rmicPath = getPathToRmic(chunk);
132     final RmicCompilerOptions options = getOptions(context);
133     final List<ModuleBuildTarget> targetsProcessed = new ArrayList<ModuleBuildTarget>(remoteClasses.size());
134
135     for (Map.Entry<ModuleBuildTarget, Collection<ClassItem>> entry : remoteClasses.entrySet()) {
136       try {
137         final ModuleBuildTarget target = entry.getKey();
138         final Collection<String> cmdLine = createStartupCommand(
139           target, rmicPath, classpathString, options, entry.getValue()
140         );
141         final Process process = Runtime.getRuntime().exec(ArrayUtil.toStringArray(cmdLine));
142         final BaseOSProcessHandler handler = new BaseOSProcessHandler(process, StringUtil.join(cmdLine, " "), null) {
143           @NotNull
144           @Override
145           protected Future<?> executeOnPooledThread(@NotNull Runnable task) {
146             return SharedThreadPool.getInstance().executeOnPooledThread(task);
147           }
148         };
149
150         final RmicOutputParser stdOutParser = new RmicOutputParser(context, getPresentableName());
151         final RmicOutputParser stdErrParser = new RmicOutputParser(context, getPresentableName());
152         handler.addProcessListener(new ProcessAdapter() {
153           @Override
154           public void onTextAvailable(ProcessEvent event, Key outputType) {
155             if (outputType == ProcessOutputTypes.STDOUT) {
156               stdOutParser.append(event.getText());
157             }
158             else if (outputType == ProcessOutputTypes.STDERR) {
159               stdErrParser.append(event.getText());
160             }
161           }
162
163           @Override
164           public void processTerminated(ProcessEvent event) {
165             super.processTerminated(event);
166           }
167         });
168         handler.startNotify();
169         handler.waitFor();
170         targetsProcessed.add(target);
171         if (stdErrParser.isErrorsReported() || stdOutParser.isErrorsReported()) {
172           break;
173         }
174         else {
175           final int exitValue = handler.getProcess().exitValue();
176           if (exitValue != 0) {
177             context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, "RMI stub generation failed"));
178             break;
179           }
180         }
181       }
182       catch (IOException e) {
183         context.processMessage(new CompilerMessage(getPresentableName(), e));
184         break;
185       }
186     }
187
188     // registering generated files
189     final Map<File, File[]> fsCache = new THashMap<File, File[]>(FileUtil.FILE_HASHING_STRATEGY);
190     for (ModuleBuildTarget target : targetsProcessed) {
191       final Collection<ClassItem> items = remoteClasses.get(target);
192       for (ClassItem item : items) {
193         File[] children = fsCache.get(item.parentDir);
194         if (children == null) {
195           children = item.parentDir.listFiles();
196           if (children == null) {
197             children = EMPTY_FILE_ARRAY;
198           }
199           fsCache.put(item.parentDir, children);
200         }
201         final Collection<File> files = item.selectGeneratedFiles(children);
202         if (!files.isEmpty()) {
203           final Collection<String> sources = item.compiledClass.getSourceFilesPaths();
204           for (File generated : files) {
205             try {
206               outputConsumer.registerOutputFile(target, generated, sources);
207             }
208             catch (IOException e) {
209               context.processMessage(new CompilerMessage(getPresentableName(), e));
210             }
211           }
212         }
213       }
214     }
215
216     return exitCode;
217   }
218
219   private static Collection<String> createStartupCommand(final ModuleBuildTarget target,
220                                                          final String compilerPath,
221                                                          final String classpath,
222                                                          final RmicCompilerOptions config,
223                                                          final Collection<ClassItem> items) {
224     final List<String> commandLine = new ArrayList<String>();
225     commandLine.add(compilerPath);
226
227     if (config.DEBUGGING_INFO) {
228       commandLine.add("-g");
229     }
230     if(config.GENERATE_IIOP_STUBS) {
231       commandLine.add("-iiop");
232     }
233     final StringTokenizer tokenizer = new StringTokenizer(config.ADDITIONAL_OPTIONS_STRING, " \t\r\n");
234     while(tokenizer.hasMoreTokens()) {
235       final String token = tokenizer.nextToken();
236       commandLine.add(token);
237     }
238
239     commandLine.add("-classpath");
240     commandLine.add(classpath);
241
242     commandLine.add("-d");
243     final File outputDir = target.getOutputDir();
244     assert outputDir != null;
245     commandLine.add(outputDir.getPath());
246
247     for (ClassItem item : items) {
248       commandLine.add(item.compiledClass.getClassName());
249     }
250     return commandLine;
251   }
252
253   private static String getPathToRmic(ModuleChunk chunk) {
254     final JpsSdk<?> sdk = chunk.representativeTarget().getModule().getSdk(JpsJavaSdkType.INSTANCE);
255     if (sdk != null) {
256       final String executable = JpsJavaSdkType.getJavaExecutable(sdk);
257       if (executable != null) {
258         final int idx = FileUtil.toSystemIndependentName(executable).lastIndexOf("/");
259         if (idx >= 0) {
260           return executable.substring(0, idx) + "/rmic";
261         }
262       }
263     }
264     return SystemProperties.getJavaHome() + "/bin/rmic";
265   }
266
267   private static boolean isRemote(CompiledClass compiled, InstrumentationClassFinder finder) throws IOException{
268     try {
269       final InstrumentationClassFinder.PseudoClass pseudoClass = finder.loadClass(compiled.getClassName());
270       if (pseudoClass != null && !pseudoClass.isInterface()) {
271         for (InstrumentationClassFinder.PseudoClass anInterface : pseudoClass.getInterfaces()) {
272           if (isRemoteInterface(anInterface, REMOTE_INTERFACE_NAME)) {
273             return true;
274           }
275         }
276       }
277     }
278     catch (ClassNotFoundException ignored) {
279     }
280     return false;
281   }
282
283   private static boolean isRemoteInterface(InstrumentationClassFinder.PseudoClass iface, final String remoteInterfaceName)
284     throws IOException, ClassNotFoundException {
285     if (remoteInterfaceName.equals(iface.getName())) {
286       return true;
287     }
288     for (InstrumentationClassFinder.PseudoClass superIface : iface.getInterfaces()) {
289       if (isRemoteInterface(superIface, remoteInterfaceName)) {
290         return true;
291       }
292     }
293     return false;
294   }
295
296   @Nullable
297   private static RmicCompilerOptions getOptions(CompileContext context) {
298     final JpsJavaCompilerConfiguration config = JpsJavaExtensionService.getInstance().getCompilerConfiguration(context.getProjectDescriptor().getProject());
299     if (config != null) {
300       final JpsJavaCompilerOptions options = config.getCompilerOptions("Rmic");
301       if (options instanceof RmicCompilerOptions) {
302         return (RmicCompilerOptions)options;
303       }
304     }
305     return null;
306   }
307
308   private static final class ClassItem {
309     static final String[] GEN_SUFFIXES = {"_Stub.class", "_Skel.class", "_Tie.class"};
310     final CompiledClass compiledClass;
311     final File parentDir;
312     final String baseName;
313
314     ClassItem(CompiledClass compiledClass) {
315       this.compiledClass = compiledClass;
316       final File outputFile = compiledClass.getOutputFile();
317       parentDir = outputFile.getParentFile();
318       baseName = StringUtil.trimEnd(outputFile.getName(), ".class");
319     }
320
321     @NotNull
322     public Collection<File> selectGeneratedFiles(File[] candidates) {
323       if (candidates == null || candidates.length == 0) {
324         return Collections.emptyList();
325       }
326       final Collection<File> result = new SmartList<File>();
327       final String[] suffixes = new String[GEN_SUFFIXES.length];
328       for (int i = 0; i < GEN_SUFFIXES.length; i++) {
329         suffixes[i] = baseName + GEN_SUFFIXES[i];
330       }
331       for (File candidate : candidates) {
332         final String name = candidate.getName();
333         for (String suffix : suffixes) {
334           if (name.endsWith(suffix)) {
335             result.add(candidate);
336             break;
337           }
338         }
339       }
340       return result;
341     }
342   }
343
344   private static class RmicOutputParser extends LineOutputWriter {
345     private final CompileContext myContext;
346     private final String myCompilerName;
347     private boolean myErrorsReported = false;
348
349     private RmicOutputParser(CompileContext context, String name) {
350       myContext = context;
351       myCompilerName = name;
352     }
353
354     private boolean isErrorsReported() {
355       return myErrorsReported;
356     }
357
358     @Override
359     protected void lineAvailable(String line) {
360       if (!StringUtil.isEmpty(line)) {
361         BuildMessage.Kind kind = BuildMessage.Kind.INFO;
362         if (line.contains("error")) {
363           kind = BuildMessage.Kind.ERROR;
364           myErrorsReported = true;
365         }
366         else if (line.contains("warning")) {
367           kind = BuildMessage.Kind.WARNING;
368         }
369         myContext.processMessage(new CompilerMessage(myCompilerName, kind, line));
370       }
371     }
372   }
373 }