diff options
author | Jenkins CI <ci@sonarsource.com> | 2016-02-15 08:11:57 +0100 |
---|---|---|
committer | Jenkins CI <ci@sonarsource.com> | 2016-02-15 08:11:57 +0100 |
commit | aa82282b15b0ea160d1e227ec1532b4cb7b49520 (patch) | |
tree | 1a07f487eec1e5254473e200e517754e71620218 /sonar-core | |
parent | 640ae85cdd30643c8c58ab092b998274422758d0 (diff) | |
parent | e93851854a5ab728fba5e83b81b7a4a24208481a (diff) | |
download | sonarqube-aa82282b15b0ea160d1e227ec1532b4cb7b49520.tar.gz sonarqube-aa82282b15b0ea160d1e227ec1532b4cb7b49520.zip |
Automatic merge from branch-5.4
* origin/branch-5.4:
Refactor a bit ComponentContainer
stabilize ProjectDrilldownTest
SONAR-7125 use efficient delete dir code in core, batch and server
SONAR-7125 use efficient delete dir from NIO in sonar-api
SONAR-7125 add FileUtils in sonar-core with efficient delete methods
SONAR-7125 use efficient delete dir method in sonar-process
SONAR-7125 use nio to clean temp directory at startup
Diffstat (limited to 'sonar-core')
4 files changed, 451 insertions, 12 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java b/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java index 3d01d8f482e..6e7d1161bb4 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/ComponentContainer.java @@ -22,6 +22,7 @@ package org.sonar.core.platform; import com.google.common.collect.Iterables; import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; import org.picocontainer.Characteristics; @@ -41,6 +42,9 @@ import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Loggers; import org.sonar.api.utils.log.Profiler; +import static com.google.common.collect.ImmutableList.copyOf; +import static java.util.Objects.requireNonNull; + @BatchSide @ServerSide public class ComponentContainer implements ContainerPopulator.Container { @@ -270,10 +274,14 @@ public class ComponentContainer implements ContainerPopulator.Container { } public ComponentContainer removeChild(ComponentContainer childToBeRemoved) { - for (ComponentContainer child : children) { + requireNonNull(childToBeRemoved); + Iterator<ComponentContainer> childrenIterator = children.iterator(); + while (childrenIterator.hasNext()) { + ComponentContainer child = childrenIterator.next(); if (child == childToBeRemoved) { - pico.removeChildContainer(child.pico); - children.remove(child); + if (pico.removeChildContainer(child.pico)) { + childrenIterator.remove(); + } break; } } @@ -281,10 +289,13 @@ public class ComponentContainer implements ContainerPopulator.Container { } private ComponentContainer removeChildren() { - for (ComponentContainer child : children) { - pico.removeChildContainer(child.pico); + Iterator<ComponentContainer> childrenIterator = children.iterator(); + while (childrenIterator.hasNext()) { + ComponentContainer child = childrenIterator.next(); + if (pico.removeChildContainer(child.pico)) { + childrenIterator.remove(); + } } - children.clear(); return this; } @@ -310,7 +321,7 @@ public class ComponentContainer implements ContainerPopulator.Container { } public List<ComponentContainer> getChildren() { - return children; + return copyOf(children); } public MutablePicoContainer getPicoContainer() { diff --git a/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java b/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java index 5f6f9820844..a1572554d86 100644 --- a/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java +++ b/sonar-core/src/main/java/org/sonar/core/util/DefaultHttpDownloader.java @@ -29,7 +29,6 @@ import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.common.io.Files; import com.google.common.io.InputSupplier; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -44,11 +43,8 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; - import javax.annotation.Nullable; - import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.sonar.api.config.Settings; import org.sonar.api.platform.Server; @@ -56,6 +52,8 @@ import org.sonar.api.utils.HttpDownloader; import org.sonar.api.utils.SonarException; import org.sonar.api.utils.log.Loggers; +import static org.sonar.core.util.FileUtils.deleteQuietly; + /** * This component downloads HTTP files * @@ -150,7 +148,7 @@ public class DefaultHttpDownloader extends HttpDownloader { try { Files.copy(downloader.newInputSupplier(uri, this.connectTimeout, this.readTimeout), toFile); } catch (IOException e) { - FileUtils.deleteQuietly(toFile); + deleteQuietly(toFile); throw failToDownload(uri, e); } } diff --git a/sonar-core/src/main/java/org/sonar/core/util/FileUtils.java b/sonar-core/src/main/java/org/sonar/core/util/FileUtils.java new file mode 100644 index 00000000000..df5e77dbe38 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/util/FileUtils.java @@ -0,0 +1,185 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.core.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.EnumSet; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +/** + * This utility class provides Java NIO based replacement for some methods of + * {@link org.apache.commons.io.FileUtils Common IO FileUtils} class. + */ +public final class FileUtils { + + private static final String DIRECTORY_CAN_NOT_BE_NULL = "Directory can not be null"; + private static final EnumSet<FileVisitOption> FOLLOW_LINKS = EnumSet.of(FileVisitOption.FOLLOW_LINKS); + + private FileUtils() { + // prevents instantiation + } + + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(File directory) throws IOException { + requireNonNull(directory, DIRECTORY_CAN_NOT_BE_NULL); + + deleteDirectoryImpl(directory.toPath()); + } + + /** + * Cleans a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void cleanDirectory(File directory) throws IOException { + requireNonNull(directory, DIRECTORY_CAN_NOT_BE_NULL); + + Path path = directory.toPath(); + if (!Files.exists(path)) { + return; + } + + cleanDirectoryImpl(path); + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + * <p> + * The difference between File.delete() and this method are: + * <ul> + * <li>A directory to be deleted does not have to be empty.</li> + * <li>No exceptions are thrown when a file or directory cannot be deleted.</li> + * </ul> + * + * @param file file or directory to delete, can be {@code null} + * @return {@code true} if the file or directory was deleted, otherwise {@code false} + */ + public static boolean deleteQuietly(@Nullable File file) { + if (file == null) { + return false; + } + + try { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + Files.delete(file.toPath()); + } + return true; + } catch (IOException | SecurityException ignored) { + return false; + } + } + + private static void checkIO(boolean condition, String pattern, Object... arguments) throws IOException { + if (!condition) { + throw new IOException(format(pattern, arguments)); + } + } + + private static void cleanDirectoryImpl(Path path) throws IOException { + checkArgument(Files.isDirectory(path), "'%s' is not a directory", path); + + Files.walkFileTree(path, FOLLOW_LINKS, CleanDirectoryFileVisitor.VISIT_MAX_DEPTH, new CleanDirectoryFileVisitor(path)); + } + + private static void deleteDirectoryImpl(Path path) throws IOException { + requireNonNull(path, DIRECTORY_CAN_NOT_BE_NULL); + if (!Files.exists(path)) { + return; + } + + checkIO(!Files.isSymbolicLink(path), "Directory '%s' is a symbolic link", path); + checkIO(!Files.isRegularFile(path), "Directory '%s' is a file", path); + + Files.walkFileTree(path, DeleteRecursivelyFileVisitor.INSTANCE); + + checkIO(!Files.exists(path), "Unable to delete directory '%s'", path); + } + + /** + * This visitor is intended to be used to visit direct children of directory <strong>or a symLink to a directory</strong>, + * so, with a max depth of {@link #VISIT_MAX_DEPTH 1}. Each direct children will either be directly deleted (if file) + * or recursively deleted (if directory). + */ + private static class CleanDirectoryFileVisitor extends SimpleFileVisitor<Path> { + public static final int VISIT_MAX_DEPTH = 1; + + private final Path path; + private final boolean symLink; + + public CleanDirectoryFileVisitor(Path path) { + this.path = path; + this.symLink = Files.isSymbolicLink(path); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (Files.isDirectory(file)) { + deleteDirectoryImpl(file); + } else if (!symLink || !file.equals(path)) { + Files.delete(file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + if (!dir.equals(path)) { + deleteDirectoryImpl(dir); + } + return FileVisitResult.CONTINUE; + } + } + + private static final class DeleteRecursivelyFileVisitor extends SimpleFileVisitor<Path> { + public static final DeleteRecursivelyFileVisitor INSTANCE = new DeleteRecursivelyFileVisitor(); + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + } + +} diff --git a/sonar-core/src/test/java/org/sonar/core/util/FileUtilsTest.java b/sonar-core/src/test/java/org/sonar/core/util/FileUtilsTest.java new file mode 100644 index 00000000000..28411c2c110 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/util/FileUtilsTest.java @@ -0,0 +1,245 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.core.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import javax.annotation.CheckForNull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FileUtilsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void cleanDirectory_throws_NPE_if_file_is_null() throws IOException { + expectDirectoryCanNotBeNullNPE(); + + FileUtils.cleanDirectory(null); + } + + @Test + public void cleanDirectory_does_nothing_if_argument_does_not_exist() throws IOException { + FileUtils.cleanDirectory(new File("/a/b/ToDoSSS")); + } + + @Test + public void cleanDirectory_throws_IAE_if_argument_is_a_file() throws IOException { + File file = temporaryFolder.newFile(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'" + file.getAbsolutePath() + "' is not a directory"); + + FileUtils.cleanDirectory(file); + } + + @Test + public void cleanDirectory_removes_directories_and_files_in_target_directory_but_not_target_directory() throws IOException { + Path target = temporaryFolder.newFolder().toPath(); + Path childFile1 = Files.createFile(target.resolve("file1.txt")); + Path childDir1 = Files.createDirectory(target.resolve("subDir1")); + Path childFile2 = Files.createFile(childDir1.resolve("file2.txt")); + Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2")); + + assertThat(target).isDirectory(); + assertThat(childFile1).isRegularFile(); + assertThat(childDir1).isDirectory(); + assertThat(childFile2).isRegularFile(); + assertThat(childDir2).isDirectory(); + + // on supporting FileSystem, target will change if directory is recreated + Object targetKey = getFileKey(target); + + FileUtils.cleanDirectory(target.toFile()); + + assertThat(target).isDirectory(); + assertThat(childFile1).doesNotExist(); + assertThat(childDir1).doesNotExist(); + assertThat(childFile2).doesNotExist(); + assertThat(childDir2).doesNotExist(); + assertThat(getFileKey(target)).isEqualTo(targetKey); + } + + @Test + public void cleanDirectory_follows_symlink_to_target_directory() throws IOException { + Path target = temporaryFolder.newFolder().toPath(); + Path symToDir = Files.createSymbolicLink(temporaryFolder.newFolder().toPath().resolve("sym_to_dir"), target); + Path childFile1 = Files.createFile(target.resolve("file1.txt")); + Path childDir1 = Files.createDirectory(target.resolve("subDir1")); + Path childFile2 = Files.createFile(childDir1.resolve("file2.txt")); + Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2")); + + assertThat(target).isDirectory(); + assertThat(symToDir).isSymbolicLink(); + assertThat(childFile1).isRegularFile(); + assertThat(childDir1).isDirectory(); + assertThat(childFile2).isRegularFile(); + assertThat(childDir2).isDirectory(); + + // on supporting FileSystem, target will change if directory is recreated + Object targetKey = getFileKey(target); + Object symLinkKey = getFileKey(symToDir); + + FileUtils.cleanDirectory(symToDir.toFile()); + + assertThat(target).isDirectory(); + assertThat(symToDir).isSymbolicLink(); + assertThat(childFile1).doesNotExist(); + assertThat(childDir1).doesNotExist(); + assertThat(childFile2).doesNotExist(); + assertThat(childDir2).doesNotExist(); + assertThat(getFileKey(target)).isEqualTo(targetKey); + assertThat(getFileKey(symToDir)).isEqualTo(symLinkKey); + } + + @Test + public void deleteQuietly_does_not_fail_if_argument_is_null() { + FileUtils.deleteQuietly(null); + } + + @Test + public void deleteQuietly_does_not_fail_if_file_does_not_exist() throws IOException { + File file = new File(temporaryFolder.newFolder(), "blablabl"); + assertThat(file).doesNotExist(); + + FileUtils.deleteQuietly(file); + } + + @Test + public void deleteQuietly_deletes_directory_and_content() throws IOException { + Path target = temporaryFolder.newFolder().toPath(); + Path childFile1 = Files.createFile(target.resolve("file1.txt")); + Path childDir1 = Files.createDirectory(target.resolve("subDir1")); + Path childFile2 = Files.createFile(childDir1.resolve("file2.txt")); + Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2")); + + assertThat(target).isDirectory(); + assertThat(childFile1).isRegularFile(); + assertThat(childDir1).isDirectory(); + assertThat(childFile2).isRegularFile(); + assertThat(childDir2).isDirectory(); + + FileUtils.deleteQuietly(target.toFile()); + + assertThat(target).doesNotExist(); + assertThat(childFile1).doesNotExist(); + assertThat(childDir1).doesNotExist(); + assertThat(childFile2).doesNotExist(); + assertThat(childDir2).doesNotExist(); + } + + @Test + public void deleteQuietly_deletes_symbolicLink() throws IOException { + Path folder = temporaryFolder.newFolder().toPath(); + Path file1 = Files.createFile(folder.resolve("file1.txt")); + Path symLink = Files.createSymbolicLink(folder.resolve("link1"), file1); + + assertThat(file1).isRegularFile(); + assertThat(symLink).isSymbolicLink(); + + FileUtils.deleteQuietly(symLink.toFile()); + + assertThat(symLink).doesNotExist(); + assertThat(file1).isRegularFile(); + } + + @Test + public void deleteDirectory_throws_NPE_if_argument_is_null() throws IOException { + expectDirectoryCanNotBeNullNPE(); + + FileUtils.deleteDirectory(null); + } + + @Test + public void deleteDirectory_does_not_fail_if_file_does_not_exist() throws IOException { + File file = new File(temporaryFolder.newFolder(), "foo.d"); + + FileUtils.deleteDirectory(file); + } + + @Test + public void deleteDirectory_throws_IOE_if_argument_is_a_file() throws IOException { + File file = temporaryFolder.newFile(); + + expectedException.expect(IOException.class); + expectedException.expectMessage("Directory '" + file.getAbsolutePath() + "' is a file"); + + FileUtils.deleteDirectory(file); + } + + @Test + public void deleteDirectory_throws_IOE_if_file_is_symbolicLink() throws IOException { + Path folder = temporaryFolder.newFolder().toPath(); + Path file1 = Files.createFile(folder.resolve("file1.txt")); + Path symLink = Files.createSymbolicLink(folder.resolve("link1"), file1); + + assertThat(file1).isRegularFile(); + assertThat(symLink).isSymbolicLink(); + + expectedException.expect(IOException.class); + expectedException.expectMessage("Directory '" + symLink.toFile().getAbsolutePath() + "' is a symbolic link"); + + FileUtils.deleteDirectory(symLink.toFile()); + } + + @Test + public void deleteDirectory_deletes_directory_and_content() throws IOException { + Path target = temporaryFolder.newFolder().toPath(); + Path childFile1 = Files.createFile(target.resolve("file1.txt")); + Path childDir1 = Files.createDirectory(target.resolve("subDir1")); + Path childFile2 = Files.createFile(childDir1.resolve("file2.txt")); + Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2")); + + assertThat(target).isDirectory(); + assertThat(childFile1).isRegularFile(); + assertThat(childDir1).isDirectory(); + assertThat(childFile2).isRegularFile(); + assertThat(childDir2).isDirectory(); + + FileUtils.deleteQuietly(target.toFile()); + + assertThat(target).doesNotExist(); + assertThat(childFile1).doesNotExist(); + assertThat(childDir1).doesNotExist(); + assertThat(childFile2).doesNotExist(); + assertThat(childDir2).doesNotExist(); + } + + private void expectDirectoryCanNotBeNullNPE() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Directory can not be null"); + } + + @CheckForNull + private static Object getFileKey(Path path) throws IOException { + BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + return attrs.fileKey(); + } +} |