001    /*
002     * Copyright 2010-2015 JetBrains s.r.o.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    package org.jetbrains.kotlin.codegen;
018    
019    import com.google.common.collect.Lists;
020    import com.intellij.openapi.vfs.VirtualFile;
021    import com.intellij.psi.PsiFile;
022    import com.intellij.util.Function;
023    import com.intellij.util.containers.ContainerUtil;
024    import com.intellij.util.io.DataOutputStream;
025    import kotlin.collections.CollectionsKt;
026    import org.jetbrains.annotations.NotNull;
027    import org.jetbrains.annotations.Nullable;
028    import org.jetbrains.annotations.TestOnly;
029    import org.jetbrains.kotlin.backend.common.output.OutputFile;
030    import org.jetbrains.kotlin.backend.common.output.OutputFileCollection;
031    import org.jetbrains.kotlin.codegen.state.GenerationState;
032    import org.jetbrains.kotlin.load.kotlin.JvmMetadataVersion;
033    import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils;
034    import org.jetbrains.kotlin.load.kotlin.PackageParts;
035    import org.jetbrains.kotlin.name.FqName;
036    import org.jetbrains.kotlin.psi.KtFile;
037    import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin;
038    import org.jetbrains.kotlin.serialization.jvm.JvmPackageTable;
039    import org.jetbrains.org.objectweb.asm.Type;
040    
041    import java.io.ByteArrayOutputStream;
042    import java.io.File;
043    import java.io.IOException;
044    import java.io.UnsupportedEncodingException;
045    import java.util.*;
046    
047    import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.getMappingFileName;
048    
049    public class ClassFileFactory implements OutputFileCollection {
050        private final GenerationState state;
051        private final ClassBuilderFactory builderFactory;
052        private final Map<FqName, PackageCodegen> package2codegen = new HashMap<FqName, PackageCodegen>();
053        private final Map<FqName, MultifileClassCodegen> multifileClass2codegen = new HashMap<FqName, MultifileClassCodegen>();
054        private final Map<String, OutAndSourceFileList> generators = new LinkedHashMap<String, OutAndSourceFileList>();
055    
056        private boolean isDone = false;
057    
058        public ClassFileFactory(@NotNull GenerationState state, @NotNull ClassBuilderFactory builderFactory) {
059            this.state = state;
060            this.builderFactory = builderFactory;
061        }
062    
063        @NotNull
064        public ClassBuilder newVisitor(
065                @NotNull JvmDeclarationOrigin origin,
066                @NotNull Type asmType,
067                @NotNull PsiFile sourceFile) {
068            return newVisitor(origin, asmType, Collections.singletonList(sourceFile));
069        }
070    
071        @NotNull
072        public ClassBuilder newVisitor(
073                @NotNull JvmDeclarationOrigin origin,
074                @NotNull Type asmType,
075                @NotNull Collection<? extends PsiFile> sourceFiles) {
076            String outputFilePath = asmType.getInternalName() + ".class";
077            List<File> ioSourceFiles = toIoFilesIgnoringNonPhysical(sourceFiles);
078            state.getProgress().reportOutput(ioSourceFiles, new File(outputFilePath));
079            ClassBuilder answer = builderFactory.newClassBuilder(origin);
080            generators.put(outputFilePath, new ClassBuilderAndSourceFileList(answer, ioSourceFiles));
081            return answer;
082        }
083    
084        void done() {
085            if (!isDone) {
086                isDone = true;
087                Collection<PackageCodegen> packageCodegens = package2codegen.values();
088                Collection<MultifileClassCodegen> multifileClassCodegens = multifileClass2codegen.values();
089                for (MultifileClassCodegen codegen : multifileClassCodegens) {
090                    codegen.done();
091                }
092                writeModuleMappings(packageCodegens, multifileClassCodegens);
093            }
094        }
095    
096        private void writeModuleMappings(
097                @NotNull Collection<PackageCodegen> packageCodegens,
098                @NotNull Collection<MultifileClassCodegen> multifileClassCodegens
099        ) {
100            final JvmPackageTable.PackageTable.Builder builder = JvmPackageTable.PackageTable.newBuilder();
101            String outputFilePath = getMappingFileName(state.getModuleName());
102    
103            List<PackageParts> parts = collectGeneratedPackageParts(packageCodegens, multifileClassCodegens);
104    
105            Set<File> sourceFiles = new HashSet<File>();
106            // TODO extract common logic
107            for (PackageCodegen codegen : packageCodegens) {
108                sourceFiles.addAll(toIoFilesIgnoringNonPhysical(PackagePartClassUtils.getFilesWithCallables(codegen.getFiles())));
109            }
110            for (MultifileClassCodegen codegen : multifileClassCodegens) {
111                sourceFiles.addAll(toIoFilesIgnoringNonPhysical(PackagePartClassUtils.getFilesWithCallables(codegen.getFiles())));
112            }
113    
114            for (PackageParts part : ClassFileUtilsKt.addCompiledPartsAndSort(parts, state)) {
115                PackageParts.Companion.serialize(part, builder);
116            }
117    
118            if (builder.getPackagePartsCount() != 0) {
119                state.getProgress().reportOutput(sourceFiles, new File(outputFilePath));
120                generators.put(outputFilePath, new OutAndSourceFileList(CollectionsKt.toList(sourceFiles)) {
121                    @Override
122                    public byte[] asBytes(ClassBuilderFactory factory) {
123                        try {
124                            ByteArrayOutputStream moduleMapping = new ByteArrayOutputStream(4096);
125                            DataOutputStream dataOutStream = new DataOutputStream(moduleMapping);
126                            int[] version = JvmMetadataVersion.INSTANCE.toArray();
127                            dataOutStream.writeInt(version.length);
128                            for (int number : version) {
129                                dataOutStream.writeInt(number);
130                            }
131                            builder.build().writeTo(dataOutStream);
132                            dataOutStream.flush();
133                            return moduleMapping.toByteArray();
134                        }
135                        catch (UnsupportedEncodingException e) {
136                            throw new RuntimeException(e);
137                        }
138                        catch (IOException e) {
139                            throw new RuntimeException(e);
140                        }
141                    }
142    
143                    @Override
144                    public String asText(ClassBuilderFactory factory) {
145                        try {
146                            return new String(asBytes(factory), "UTF-8");
147                        }
148                        catch (UnsupportedEncodingException e) {
149                            throw new RuntimeException(e);
150                        }
151                    }
152                });
153            }
154        }
155    
156        private static List<PackageParts> collectGeneratedPackageParts(
157                @NotNull Collection<PackageCodegen> packageCodegens,
158                @NotNull Collection<MultifileClassCodegen> multifileClassCodegens
159        ) {
160            Map<String, PackageParts> mergedPartsByPackageName = new LinkedHashMap<String, PackageParts>();
161    
162            for (PackageCodegen packageCodegen : packageCodegens) {
163                PackageParts generatedParts = packageCodegen.getPackageParts();
164                PackageParts premergedParts = new PackageParts(generatedParts.getPackageFqName());
165                mergedPartsByPackageName.put(generatedParts.getPackageFqName(), premergedParts);
166                premergedParts.getParts().addAll(generatedParts.getParts());
167            }
168    
169            for (MultifileClassCodegen multifileClassCodegen : multifileClassCodegens) {
170                PackageParts multifileClassParts = multifileClassCodegen.getPackageParts();
171                PackageParts premergedParts = mergedPartsByPackageName.get(multifileClassParts.getPackageFqName());
172                if (premergedParts == null) {
173                    premergedParts = new PackageParts(multifileClassParts.getPackageFqName());
174                    mergedPartsByPackageName.put(multifileClassParts.getPackageFqName(), premergedParts);
175                }
176                premergedParts.getParts().addAll(multifileClassParts.getParts());
177            }
178    
179            List<PackageParts> result = new ArrayList<PackageParts>();
180            result.addAll(mergedPartsByPackageName.values());
181            return result;
182        }
183    
184        @NotNull
185        @Override
186        public List<OutputFile> asList() {
187            done();
188            return ContainerUtil.map(generators.keySet(), new Function<String, OutputFile>() {
189                @Override
190                public OutputFile fun(String relativeClassFilePath) {
191                    return new OutputClassFile(relativeClassFilePath);
192                }
193            });
194        }
195    
196        @Override
197        @Nullable
198        public OutputFile get(@NotNull String relativePath) {
199            return generators.containsKey(relativePath) ? new OutputClassFile(relativePath) : null;
200        }
201    
202        @NotNull
203        @TestOnly
204        public String createText() {
205            StringBuilder answer = new StringBuilder();
206    
207            for (OutputFile file : asList()) {
208                answer.append("@").append(file.getRelativePath()).append('\n');
209                answer.append(file.asText());
210            }
211    
212            return answer.toString();
213        }
214    
215        @NotNull
216        @TestOnly
217        public Map<String, String> createTextForEachFile() {
218            Map<String, String> answer = new LinkedHashMap<String, String>();
219            for (OutputFile file : asList()) {
220                answer.put(file.getRelativePath(), file.asText());
221            }
222            return answer;
223        }
224    
225        @NotNull
226        public PackageCodegen forPackage(@NotNull FqName fqName, @NotNull Collection<KtFile> files) {
227            assert !isDone : "Already done!";
228            PackageCodegen codegen = package2codegen.get(fqName);
229            if (codegen == null) {
230                codegen = new PackageCodegen(state, files, fqName);
231                package2codegen.put(fqName, codegen);
232            }
233    
234            return codegen;
235        }
236    
237        @NotNull
238        public MultifileClassCodegen forMultifileClass(@NotNull FqName facadeFqName, @NotNull Collection<KtFile> files) {
239            assert !isDone : "Already done!";
240            MultifileClassCodegen codegen = multifileClass2codegen.get(facadeFqName);
241            if (codegen == null) {
242                codegen = new MultifileClassCodegen(state, files, facadeFqName);
243                multifileClass2codegen.put(facadeFqName, codegen);
244            }
245            return codegen;
246        }
247    
248        @NotNull
249        private static List<File> toIoFilesIgnoringNonPhysical(@NotNull Collection<? extends PsiFile> psiFiles) {
250            List<File> result = Lists.newArrayList();
251            for (PsiFile psiFile : psiFiles) {
252                VirtualFile virtualFile = psiFile.getVirtualFile();
253                // We ignore non-physical files here, because this code is needed to tell the make what inputs affect which outputs
254                // a non-physical file cannot be processed by make
255                if (virtualFile != null) {
256                    result.add(new File(virtualFile.getPath()));
257                }
258            }
259            return result;
260        }
261    
262        private class OutputClassFile implements OutputFile {
263            private final String relativeClassFilePath;
264    
265            public OutputClassFile(String relativeClassFilePath) {
266                this.relativeClassFilePath = relativeClassFilePath;
267            }
268    
269            @NotNull
270            @Override
271            public String getRelativePath() {
272                return relativeClassFilePath;
273            }
274    
275            @NotNull
276            @Override
277            public List<File> getSourceFiles() {
278                OutAndSourceFileList pair = generators.get(relativeClassFilePath);
279                if (pair == null) {
280                    throw new IllegalStateException("No record for binary file " + relativeClassFilePath);
281                }
282    
283                return pair.sourceFiles;
284            }
285    
286            @NotNull
287            @Override
288            public byte[] asByteArray() {
289                try {
290                    return generators.get(relativeClassFilePath).asBytes(builderFactory);
291                }
292                catch (RuntimeException e) {
293                    throw new RuntimeException("Error generating class file " + this.toString() + ": " + e.getMessage(), e);
294                }
295            }
296    
297            @NotNull
298            @Override
299            public String asText() {
300                try {
301                    return generators.get(relativeClassFilePath).asText(builderFactory);
302                }
303                catch (RuntimeException e) {
304                    throw new RuntimeException("Error generating class file " + this.toString() + ": " + e.getMessage(), e);
305                }
306            }
307    
308            @NotNull
309            @Override
310            public String toString() {
311                return getRelativePath() + " (compiled from " + getSourceFiles() + ")";
312            }
313        }
314    
315        private static final class ClassBuilderAndSourceFileList extends OutAndSourceFileList {
316            private final ClassBuilder classBuilder;
317    
318            private ClassBuilderAndSourceFileList(ClassBuilder classBuilder, List<File> sourceFiles) {
319                super(sourceFiles);
320                this.classBuilder = classBuilder;
321            }
322    
323            @Override
324            public byte[] asBytes(ClassBuilderFactory factory) {
325                return factory.asBytes(classBuilder);
326            }
327    
328            @Override
329            public String asText(ClassBuilderFactory factory) {
330                return factory.asText(classBuilder);
331            }
332        }
333    
334        private static abstract class OutAndSourceFileList {
335    
336            protected final List<File> sourceFiles;
337    
338            private OutAndSourceFileList(List<File> sourceFiles) {
339                this.sourceFiles = sourceFiles;
340            }
341    
342            public abstract byte[] asBytes(ClassBuilderFactory factory);
343    
344            public abstract String asText(ClassBuilderFactory factory);
345        }
346    
347        public void removeClasses(Set<String> classNamesToRemove) {
348            for (String classInternalName : classNamesToRemove) {
349                generators.remove(classInternalName + ".class");
350            }
351        }
352    
353        @TestOnly
354        public List<KtFile> getInputFiles() {
355            return state.getFiles();
356        }
357    }