/*
* Copyright 2000-2009 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 com.intellij.compiler.impl.packagingCompiler;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompilerBundle;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.packaging.impl.compiler.ArtifactCompilerUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.graph.CachingSemiGraph;
import com.intellij.util.graph.DFSTBuilder;
import com.intellij.util.graph.GraphGenerator;
import consulo.compiler.impl.packagingCompiler.ArchivePackageWriterEx;
import consulo.packaging.elements.ArchivePackageWriter;
import consulo.packaging.impl.util.DeploymentUtilImpl;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.*;
/**
* @author nik
*/
public class ArchivesBuilder {
public static final Logger LOGGER = Logger.getInstance(ArchivesBuilder.class);
private final Set<ArchivePackageInfo> myArchivesToBuild;
private final FileFilter myFileFilter;
private final CompileContext myContext;
private Map<ArchivePackageInfo, File> myBuiltArchives;
public ArchivesBuilder(@NotNull Set<ArchivePackageInfo> archivesToBuild, @NotNull FileFilter fileFilter, @NotNull CompileContext context) {
DependentArchivesEvaluator evaluator = new DependentArchivesEvaluator();
for (ArchivePackageInfo archivePackageInfo : archivesToBuild) {
evaluator.addArchiveWithDependencies(archivePackageInfo);
}
myArchivesToBuild = evaluator.getArchivePackageInfos();
myFileFilter = fileFilter;
myContext = context;
}
public boolean buildArchives(Set<String> writtenPaths) throws IOException {
myContext.getProgressIndicator().setText(CompilerBundle.message("packaging.compiler.message.building.archives"));
final ArchivePackageInfo[] sortedArchives = sortArchives();
if (sortedArchives == null) {
return false;
}
myBuiltArchives = new HashMap<>();
try {
for (ArchivePackageInfo archivePackageInfo : sortedArchives) {
myContext.getProgressIndicator().checkCanceled();
buildArchive(archivePackageInfo);
}
myContext.getProgressIndicator().setText(CompilerBundle.message("packaging.compiler.message.copying.archives"));
copyJars(writtenPaths);
}
finally {
deleteTemporaryJars();
}
return true;
}
private void deleteTemporaryJars() {
for (File file : myBuiltArchives.values()) {
FileUtil.delete(file);
}
}
private void copyJars(final Set<String> writtenPaths) throws IOException {
for (Map.Entry<ArchivePackageInfo, File> entry : myBuiltArchives.entrySet()) {
File fromFile = entry.getValue();
boolean first = true;
for (DestinationInfo destination : entry.getKey().getAllDestinations()) {
if (destination instanceof ExplodedDestinationInfo) {
File toFile = new File(FileUtil.toSystemDependentName(destination.getOutputPath()));
if (first) {
first = false;
renameFile(fromFile, toFile, writtenPaths);
fromFile = toFile;
}
else {
DeploymentUtilImpl.copyFile(fromFile, toFile, myContext, writtenPaths, myFileFilter);
}
}
}
}
}
private static void renameFile(final File fromFile, final File toFile, final Set<String> writtenPaths) throws IOException {
FileUtil.rename(fromFile, toFile);
writtenPaths.add(toFile.getPath());
}
@Nullable
private ArchivePackageInfo[] sortArchives() {
final DFSTBuilder<ArchivePackageInfo> builder = new DFSTBuilder<>(GraphGenerator.create(CachingSemiGraph.create(new ArchivesGraph())));
if (!builder.isAcyclic()) {
final Pair<ArchivePackageInfo, ArchivePackageInfo> dependency = builder.getCircularDependency();
String message = CompilerBundle
.message("packaging.compiler.error.cannot.build.circular.dependency.found.between.0.and.1", dependency.getFirst().getPresentableDestination(),
dependency.getSecond().getPresentableDestination());
myContext.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1);
return null;
}
ArchivePackageInfo[] archives = myArchivesToBuild.toArray(new ArchivePackageInfo[myArchivesToBuild.size()]);
Arrays.sort(archives, builder.comparator());
archives = ArrayUtil.reverseArray(archives);
return archives;
}
public Set<ArchivePackageInfo> getArchivesToBuild() {
return myArchivesToBuild;
}
@SuppressWarnings("unchecked")
private <T> void buildArchive(final ArchivePackageInfo archive) throws IOException {
if (archive.getPackedFiles().isEmpty() && archive.getPackedArchives().isEmpty()) {
myContext.addMessage(CompilerMessageCategory.WARNING, "Archive '" + archive.getPresentableDestination() + "' has no files so it won't be created", null,
-1, -1);
return;
}
myContext.getProgressIndicator().setText(CompilerBundle.message("packaging.compiler.message.building.0", archive.getPresentableDestination()));
File tempFile = File.createTempFile("artifactCompiler", "tmp");
myBuiltArchives.put(archive, tempFile);
FileUtil.createParentDirs(tempFile);
ArchivePackageWriter<T> packageWriter = (ArchivePackageWriter<T>)archive.getPackageWriter();
T archiveFile;
if (packageWriter instanceof ArchivePackageWriterEx) {
archiveFile = ((ArchivePackageWriterEx<T>)packageWriter).createArchiveObject(tempFile, archive);
}
else {
archiveFile = packageWriter.createArchiveObject(tempFile);
}
try {
final THashSet<String> writtenPaths = new THashSet<>();
for (Pair<String, VirtualFile> pair : archive.getPackedFiles()) {
final VirtualFile sourceFile = pair.getSecond();
if (sourceFile.isInLocalFileSystem()) {
File file = VfsUtil.virtualToIoFile(sourceFile);
addFileToArchive(archiveFile, packageWriter, file, pair.getFirst(), writtenPaths);
}
else {
extractFileAndAddToArchive(archiveFile, packageWriter, sourceFile, pair.getFirst(), writtenPaths);
}
}
for (Pair<String, ArchivePackageInfo> nestedArchive : archive.getPackedArchives()) {
File nestedArchiveFile = myBuiltArchives.get(nestedArchive.getSecond());
if (nestedArchiveFile != null) {
addFileToArchive(archiveFile, packageWriter, nestedArchiveFile, nestedArchive.getFirst(), writtenPaths);
}
else {
LOGGER.debug("nested archive file " + nestedArchive.getFirst() + " for " + archive.getPresentableDestination() + " not found");
}
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
packageWriter.close(archiveFile);
}
}
private <T> void extractFileAndAddToArchive(@NotNull T archiveObject,
@NotNull ArchivePackageWriter<T> writer,
VirtualFile sourceFile,
String relativePath,
THashSet<String> writtenPaths) throws IOException {
relativePath = addParentDirectories(archiveObject, writer, writtenPaths, relativePath);
myContext.getProgressIndicator().setText2(relativePath);
if (!writtenPaths.add(relativePath)) return;
Pair<InputStream, Long> streamLongPair = ArtifactCompilerUtil.getArchiveEntryInputStream(sourceFile, myContext);
final InputStream input = streamLongPair.getFirst();
if (input == null) {
return;
}
try {
writer.addFile(archiveObject, input, relativePath, streamLongPair.getSecond(), ArtifactCompilerUtil.getArchiveFile(sourceFile).lastModified());
}
finally {
input.close();
}
}
private <T> void addFileToArchive(@NotNull T archiveObject,
@NotNull ArchivePackageWriter<T> writer,
@NotNull File file,
@NotNull String relativePath,
@NotNull THashSet<String> writtenPaths) throws IOException {
if (!file.exists()) {
return;
}
if (!FileUtil.isFilePathAcceptable(file, myFileFilter)) {
return;
}
if (!writtenPaths.add(relativePath)) {
return;
}
relativePath = addParentDirectories(archiveObject, writer, writtenPaths, relativePath);
myContext.getProgressIndicator().setText2(relativePath);
try (FileInputStream fileOutputStream = new FileInputStream(file)) {
writer.addFile(archiveObject, fileOutputStream, relativePath, file.length(), file.lastModified());
}
}
private static <T> String addParentDirectories(@NotNull T archiveObject,
@NotNull ArchivePackageWriter<T> writer,
THashSet<String> writtenPaths,
String relativePath) throws IOException {
while (StringUtil.startsWithChar(relativePath, '/')) {
relativePath = relativePath.substring(1);
}
int i = relativePath.indexOf('/');
while (i != -1) {
String prefix = relativePath.substring(0, i + 1);
if (!writtenPaths.contains(prefix) && prefix.length() > 1) {
writer.addDirectory(archiveObject, prefix);
writtenPaths.add(prefix);
}
i = relativePath.indexOf('/', i + 1);
}
return relativePath;
}
private class ArchivesGraph implements GraphGenerator.SemiGraph<ArchivePackageInfo> {
@Override
public Collection<ArchivePackageInfo> getNodes() {
return myArchivesToBuild;
}
@Override
public Iterator<ArchivePackageInfo> getIn(final ArchivePackageInfo n) {
Set<ArchivePackageInfo> ins = new HashSet<>();
for (ArchiveDestinationInfo destination : n.getArchiveDestinations()) {
ins.add(destination.getArchivePackageInfo());
}
return ins.iterator();
}
}
}