/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.make; import com.intellij.compiler.instrumentation.FailSafeClassReader; import com.intellij.compiler.instrumentation.InstrumentationClassFinder; import com.intellij.compiler.instrumentation.InstrumenterClassWriter; import com.intellij.compiler.notNullVerification.NotNullVerifyingInstrumenter; import jetbrains.mps.make.CompilationErrorsHandler.ClassesErrorsTracker; import jetbrains.mps.project.MPSExtentions; import jetbrains.mps.reloading.IClassPathItem; import jetbrains.mps.reloading.RealClassPathItem; import jetbrains.mps.util.NameUtil; import jetbrains.mps.vfs.IFile; import org.eclipse.jdt.internal.compiler.ClassFile; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.org.objectweb.asm.ClassWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import static jetbrains.mps.project.SModuleOperations.getJavaFacet; /** * Write compiled java classes to disk, also instruments the notnull annotations * * fixme use bundle for this package * Created by apyshkin on 5/26/16. */ public class ClassFileWriter { private final static String OUTPUT_DIR_CANNOT_BE_CREATED = "Can't create %s directory"; private final static String MODULE_FOR_CLASS_NOT_FOUND = "It cannot be calculated in which module's output path the class file for %s must be placed"; private final static String OUTPUT_DIR_IS_NOT_WRITEABLE = "Can't write to %s"; private final static String OUTPUT_CANNOT_BE_DELETED = "Can't delete %s"; private final ModulesContainer myModulesContainer; private final MessageSender mySender; private final ChangedModulesTracker myChangedModulesTracker = new ChangedModulesTracker(); private final InstrumentationClassFinder myFinder; private final Map<String, InputStream> myClassFile2Bytes = new LinkedHashMap<>(); // fixme think about class path public ClassFileWriter(ModulesContainer modulesContainer, CompositeTracer tracer, IClassPathItem classPath) { myModulesContainer = modulesContainer; mySender = tracer.getSender(); myFinder = createInstrumentationClassFinder(classPath); } @NotNull private InstrumentationClassFinder createInstrumentationClassFinder(final IClassPathItem classPath) { final URL[] urlsArr = convertClassPathToUrls(classPath); return new InstrumentationClassFinder(urlsArr) { // fixme separate platform cp from usual cp @Override protected InputStream lookupClassBeforeClasspath(String internalClassName) { return myClassFile2Bytes.get(internalClassName); } }; } @NotNull private static URL[] convertClassPathToUrls(IClassPathItem classPath) { final List<URL> urls = new ArrayList<>(); for (RealClassPathItem flatten : classPath.flatten()) { try { urls.add(new File(flatten.getPath()).toURI().toURL()); } catch (MalformedURLException e) { e.printStackTrace(); } } return urls.toArray(new URL[urls.size()]); } private void updateClassFile2BytesMap(List<CompilationResult> results) { for (CompilationResult result : results) { for (ClassFile classFile : result.getClassFiles()) { String path = convertCompoundToPath(classFile.getCompoundName()); myClassFile2Bytes.put(path, new ByteArrayInputStream(classFile.getBytes())); } } } /** * @return a set of changed modules */ @NotNull public Set<SModule> write(List<CompilationResult> results, ClassesErrorsTracker errorsTracker) { updateClassFile2BytesMap(results); for (CompilationResult result : results) { for (ClassFile cf : result.getClassFiles()) { writeClassFile(cf, errorsTracker); } } return myChangedModulesTracker.getModules(); } private void writeClassFile(@NotNull ClassFile cf, ClassesErrorsTracker errorsTracker) { String fqName = convertCompoundToFqName(cf.getCompoundName()); String containerClassName = getContainerClassName(fqName); // the name up to dollar sign SModule moduleForClass = myModulesContainer.getModuleContainingClass(containerClassName); if (moduleForClass == null) { mySender.error(String.format(MODULE_FOR_CLASS_NOT_FOUND, fqName)); } else { myChangedModulesTracker.addChanged(moduleForClass); File outputDir = createOutputDir(fqName, moduleForClass); String className = NameUtil.shortNameFromLongName(fqName); File output = new File(outputDir, className + MPSExtentions.DOT_CLASSFILE); if (!errorsTracker.hasError(containerClassName)) { writeClassFile(cf, output); } else { if (output.exists() && !output.delete()) { String errMsg = String.format(OUTPUT_CANNOT_BE_DELETED, output.getPath()); mySender.error(errMsg); } } } } private void writeClassFile(ClassFile classFile, File output) { FileOutputStream os = null; try { os = new FileOutputStream(output); byte[] classContent = instrumentNotNull(classFile.getBytes()); os.write(classContent); } catch (IOException e) { mySender.error(String.format(OUTPUT_DIR_IS_NOT_WRITEABLE, output.getAbsolutePath())); } finally { assert os != null; try { os.close(); } catch (IOException e) { mySender.error("IOException: ", e); } } } @NotNull private File createOutputDir(String fqName, SModule m) { File classesGen = getClassesGen(m); String packageName = NameUtil.namespaceFromLongName(fqName); File outputDir = new File(classesGen, NameUtil.pathFromNamespace(packageName)); if (!outputDir.exists() && !outputDir.mkdirs()) { throw new RuntimeException(String.format(OUTPUT_DIR_CANNOT_BE_CREATED, outputDir.getPath())); } return outputDir; } @NotNull private File getClassesGen(@NotNull SModule m) { IFile classesGen = getJavaFacet(m).getClassesGen(); assert classesGen != null; return new File(classesGen.getPath()); } /** * cuts the name up to the $ sign */ @NotNull private static String getContainerClassName(String fqName) { String containerClassName = fqName; if (containerClassName.contains("$")) { int index = containerClassName.indexOf('$'); containerClassName = containerClassName.substring(0, index); } return containerClassName; } // FIXME @NotNull private byte[] instrumentNotNull(@NotNull byte[] classContent) throws MalformedURLException { FailSafeClassReader reader = new FailSafeClassReader(classContent, 0, classContent.length); ClassWriter writer = new InstrumenterClassWriter(reader, ClassWriter.COMPUTE_FRAMES, myFinder); // To understand why last parameter was added - see commits 250331a & 490d4e6 in IDEA Community NotNullVerifyingInstrumenter.processClassFile(reader, writer, new String[]{NotNull.class.getName()}); return writer.toByteArray(); // return classContent; } @NotNull public static String convertCompoundToFqName(char[][] compoundName) { return convertCompoundToStringWithSep(compoundName, '.'); } private static String convertCompoundToPath(char[][] compoundName) { return convertCompoundToStringWithSep(compoundName, '/'); } private static String convertCompoundToStringWithSep(char[][] compoundName, char separator) { StringBuilder result = new StringBuilder(); for (int i = 0; i < compoundName.length; i++) { char[] part = compoundName[i]; result.append(part); if (i != compoundName.length - 1) { result.append(separator); } } return result.toString(); } private static class ChangedModulesTracker { private final Set<SModule> myModules = new HashSet<SModule>(); public void addChanged(@NotNull SModule module) { myModules.add(module); } public Set<SModule> getModules() { return myModules; } } }