blob: 0dba8ea570bc1a5e66ee04306c0544d970cacc92 [file] [log] [blame]
/*
* Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.enterprise.deploy.shared;
import com.sun.enterprise.module.ModulesRegistry;
import com.sun.enterprise.module.single.StaticModulesRegistry;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.glassfish.api.deployment.archive.WritableArchive;
import org.glassfish.hk2.api.ServiceLocator;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.glassfish.deployment.common.DeploymentContextImpl.deplLogger;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* @author Tim Quinn
*/
public class FileArchiveTest {
private static final String EXPECTED_LOG_KEY = "NCLS-DEPLOYMENT-00022";
private static final String LINE_SEP = System.getProperty("line.separator");
private static final String STALE_ENTRY = "oldLower/oldFile.txt";
private static final String SUBARCHIVE_NAME = "subarch";
private File archiveDir;
private final Set<String> usualEntryNames =
new HashSet<>(Arrays.asList(new String[] {"sample.txt", "lower/other.txt"}));
private final Set<String> usualExpectedEntryNames = initUsualExpectedEntryNames();
private final Set<String> usualExpectedEntryNamesWithOverwrittenStaleEntry =
initUsualExpectedEntryNamesWithOverwrittenStaleEntry();
private final Set<String> usualSubarchiveEntryNames =
new HashSet<>(Arrays.asList(new String[] {"a.txt", "under/b.txt"}));
private final Set<String> usualExpectedSubarchiveEntryNames = initUsualExpectedSubarchiveEntryNames();
private static ServiceLocator locator;
private static ModulesRegistry registry;
private static ArchiveFactory archiveFactory;
private static RecordingHandler handler;
@BeforeAll
public static void setUpClass() throws Exception {
registry = new StaticModulesRegistry(FileArchiveTest.class.getClassLoader());
locator = registry.createServiceLocator("default");
archiveFactory = locator.getService(ArchiveFactory.class);
handler = new RecordingHandler();
deplLogger.addHandler(handler);
}
@BeforeEach
public void setUp() throws IOException {
archiveDir = tempDir();
}
@AfterEach
public void tearDown() {
if (archiveDir != null) {
clean(archiveDir);
}
archiveDir = null;
}
@AfterAll
public static void shutdownLocator() {
if (locator != null) {
locator.shutdown();
}
if (registry != null) {
registry.shutdown();
}
}
private Set<String> initUsualExpectedEntryNames() {
final Set<String> expectedEntryNames = new HashSet<>(usualEntryNames);
expectedEntryNames.add("lower");
return expectedEntryNames;
}
private Set<String> initUsualExpectedEntryNamesWithOverwrittenStaleEntry() {
final Set<String> result = initUsualExpectedEntryNames();
result.add(STALE_ENTRY);
result.add("oldLower");
return result;
}
private Set<String> initUsualExpectedSubarchiveEntryNames() {
final Set<String> result = new HashSet<>(usualSubarchiveEntryNames);
result.add("under");
return result;
}
private File tempDir() throws IOException {
final File f = File.createTempFile("FileArch", "");
f.delete();
f.mkdir();
return f;
}
private void clean(final File dir) {
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
clean(f);
}
if ( ! f.delete()) {
f.deleteOnExit();
}
}
if ( ! dir.delete()) {
dir.deleteOnExit();
}
}
private ReadableArchive createAndPopulateArchive(final Set<String> entryNames) throws Exception {
WritableArchive instance = archiveFactory.createArchive(archiveDir.toURI());
instance.create(archiveDir.toURI());
// Add some entries.
for (String entryName : entryNames) {
instance.putNextEntry(entryName);
instance.closeEntry();
}
instance.close();
return archiveFactory.openArchive(archiveDir);
}
private ReadableArchive createAndPopulateSubarchive(
final WritableArchive parent,
final String subarchiveName,
final Set<String> entryNames) throws Exception {
final WritableArchive result = parent.createSubArchive(subarchiveName);
for (String entryName : entryNames) {
result.putNextEntry(entryName);
result.closeEntry();
}
result.close();
final ReadableArchive readableParent = archiveFactory.openArchive(parent.getURI());
return readableParent.getSubArchive(subarchiveName);
}
private void createAndPopulateAndCheckArchive(final Set<String> entryNames) throws Exception {
final ReadableArchive instance = createAndPopulateArchive(entryNames);
checkArchive(instance, usualExpectedEntryNames);
}
private void checkArchive(final ReadableArchive instance, final Set<String> expectedEntryNames) {
final Set<String> foundEntryNames = new HashSet<>();
for (Enumeration<String> e = instance.entries(); e.hasMoreElements();) {
foundEntryNames.add(e.nextElement());
}
assertEquals(expectedEntryNames, foundEntryNames, "Missing or unexpected entry names reported");
}
private void getListOfFiles(final FileArchive instance, final Set<String> expectedEntryNames, final Logger logger) {
final List<String> foundEntryNames = new ArrayList<>();
instance.getListOfFiles(archiveDir, foundEntryNames, null, logger);
assertEquals(expectedEntryNames, new HashSet<>(foundEntryNames), "Missing or unexpected entry names reported");
}
private void getListOfFilesCheckForLogRecord(FileArchive instance, final Set<String> expectedEntryNames) throws IOException {
handler.flush();
getListOfFiles(instance, expectedEntryNames, deplLogger);
if (handler.logRecords().size() != 1) {
final StringBuilder sb = new StringBuilder();
for (LogRecord record : handler.logRecords()) {
sb.append(record.getLevel().getLocalizedName())
.append(": ")
.append(record.getMessage())
.append(LINE_SEP);
}
fail("Expected 1 log message but received " + handler.logRecords().size() + " as follows:" + LINE_SEP + sb);
}
// We have a stale file under a stale directory. Make sure a direct
// request for the stale file fails. (We know already from above that
// getting the entries list triggers a warning about the skipped stale file.)
final InputStream is = instance.getEntry(STALE_ENTRY);
assertNull(is, "Incorrectly located stale FileArchive entry " + STALE_ENTRY);
}
/**
* Computes the expected entry names for an archive which contains a subarchive.
* <p>
* The archive's entries method will report all the entries in the main
* archive, plus the subarchive name, plus the entries in the subarchive.
* @param expectedFromArchive entries from the main archive
* @param subarchiveName name of the subarchive
* @param expectedFromSubarchive entries in the subarchive
* @return entry names that should be returned from the main archive's entries() method
*/
private Set<String> expectedEntryNames(Set<String> expectedFromArchive, final String subarchiveName, Set<String>expectedFromSubarchive) {
final Set<String> result = new HashSet<>(expectedFromArchive);
result.add(subarchiveName);
for (String expectedSubarchEntryName : expectedFromSubarchive) {
final StringBuilder path = new StringBuilder();
path.append(subarchiveName).append("/");
final String[] segments = expectedSubarchEntryName.split("/");
for (int i = 0; i < segments.length; i++) {
path.append(segments[i]);
result.add(path.toString());
if (i < segments.length) {
path.append("/");
}
}
}
return result;
}
@Test
public void testSubarchive() throws Exception {
final ArchiveAndSubarchive archives = createAndPopulateArchiveAndSubarchive();
checkArchive(archives.parent, archives.fullExpectedEntryNames);
checkArchive(archives.subarchive, usualExpectedSubarchiveEntryNames);
}
@Test
public void testSubArchiveCreateWithStaleEntry() throws Exception {
// Subarchives are a little tricky. The marker file lives only at
// the top level (because that's where undeployment puts it). So
// when a subarchive tests to see if an entry is valid it needs to
// consult the marker file (if any) in the top-level owning archive.
//
// This test creates a directory structure containing a stale file
// in a lower-level directory, creates the top-level marker file
// as undeployment would, then creates an archive for the top level
// and a subarchive for the lower-level directory (as the next
// deployment would). The archive and subarchive need to skip the
// stale file.
// Create a file in the directory before creating the archive.
final File oldDir = new File(archiveDir, SUBARCHIVE_NAME);
final File oldFile = new File(oldDir, STALE_ENTRY);
oldFile.getParentFile().mkdirs();
oldFile.createNewFile();
// Mimic what undeployment does by creating a marker file for the
// archive recording the pre-existing file.
FileArchive.StaleFileManager.Util.markDeletedArchive(archiveDir);
// Now create the archive and subarchive on top of the directories
// which already exist and contain the stale file and directory.
final ArchiveAndSubarchive archives = createAndPopulateArchiveAndSubarchive();
checkArchive(archives.parent, archives.fullExpectedEntryNames);
checkArchive(archives.subarchive, usualExpectedSubarchiveEntryNames);
getListOfFilesCheckForLogRecord((FileArchive) archives.parent, archives.fullExpectedEntryNames);
}
private static class ArchiveAndSubarchive {
ReadableArchive parent;
ReadableArchive subarchive;
Set<String> fullExpectedEntryNames;
}
private ArchiveAndSubarchive createAndPopulateArchiveAndSubarchive() throws Exception {
final ArchiveAndSubarchive result = new ArchiveAndSubarchive();
result.parent = createAndPopulateArchive(usualEntryNames);
result.subarchive = createAndPopulateSubarchive(
(FileArchive) result.parent,
SUBARCHIVE_NAME,
usualSubarchiveEntryNames);
result.fullExpectedEntryNames = expectedEntryNames(
usualExpectedEntryNames, SUBARCHIVE_NAME, usualSubarchiveEntryNames);
return result;
}
/**
* Test of open method, of class FileArchive.
*/
@Test
public void testNormalCreate() throws Exception {
createAndPopulateAndCheckArchive(usualEntryNames);
}
@Test
public void testCreateWithOlderLeftoverEntry() throws Exception {
final ReadableArchive instance = createWithOlderLeftoverEntry(usualEntryNames);
getListOfFilesCheckForLogRecord((FileArchive) instance, usualExpectedEntryNames);
}
@Test
public void testCreateWithOlderLeftoverEntryWhichIsCreatedAgain() throws Exception {
final FileArchive instance = (FileArchive) createWithOlderLeftoverEntry(usualEntryNames);
// Now add the stale entry explicitly which should make it valid.
try (OutputStream os = instance.putNextEntry(STALE_ENTRY)) {
os.write("No longer stale!".getBytes());
}
checkArchive(instance, usualExpectedEntryNamesWithOverwrittenStaleEntry);
}
private ReadableArchive createWithOlderLeftoverEntry(final Set<String> entryNames) throws Exception {
// Create a file in the directory before creating the archive.
final File oldFile = new File(archiveDir, STALE_ENTRY);
oldFile.getParentFile().mkdirs();
oldFile.createNewFile();
// Mimic what undeployment does by creating a marker file for the
// archive recording the pre-existing file.
FileArchive.StaleFileManager.Util.markDeletedArchive(archiveDir);
// Now create the archive. The archive should not see the old file.
return createAndPopulateArchive(entryNames);
}
@Test
public void testCreateWithOlderLeftoverEntryAndThenOpen() throws Exception {
createWithOlderLeftoverEntry(usualEntryNames);
final FileArchive openedArchive = new FileArchive();
openedArchive.open(archiveDir.toURI());
System.err.println("A WARNING should appear next");
checkArchive(openedArchive, usualExpectedEntryNames);
}
@Test
public void testOpenWithPreexistingDir() throws Exception {
createPreexistingDir();
final FileArchive openedArchive = new FileArchive();
openedArchive.open(archiveDir.toURI());
checkArchive(openedArchive, usualExpectedEntryNames);
}
private void createPreexistingDir() throws IOException {
for (String entryName : usualEntryNames) {
final File f = fileForPath(archiveDir, entryName);
final File parentDir = f.getParentFile();
if(parentDir != null) {
parentDir.mkdirs();
}
try {
f.createNewFile();
} catch (Exception ex) {
throw new IOException(f.getAbsolutePath(), ex);
}
}
}
private File fileForPath(File anchor, final String path) {
final String[] interveningDirNames = path.split("/");
File interveningDir = anchor;
for (int i = 0; i < interveningDirNames.length - 1; i++) {
String name = interveningDirNames[i];
interveningDir = new File(interveningDir, name + "/");
}
return new File(interveningDir,interveningDirNames[interveningDirNames.length - 1]);
}
@Test
public void testInaccessibleDirectoryInFileArchive() throws Exception {
final FileArchive archive = (FileArchive) createAndPopulateArchive(usualEntryNames);
// Now make the lower-level directory impossible to execute - therefore
// the attempt to list the files should fail.
final File lower = new File(archiveDir, "lower");
lower.setExecutable(false, false);
assertTrue(lower.setReadable(false, false));
// Try to list the files. This should fail with our logger getting one record.
final Vector<String> fileList = new Vector<>();
handler.flush();
archive.getListOfFiles(lower, fileList, null /* embeddedArchives */, deplLogger);
List<LogRecord> logRecords = handler.logRecords();
assertThat("FileArchive logged no message about being unable to list files; expected " + EXPECTED_LOG_KEY,
logRecords, not(emptyIterable()));
assertEquals(EXPECTED_LOG_KEY, logRecords.get(0).getMessage(),
"FileArchive did not log expected message (re: being unable to list files)");
// Change the protection back.
lower.setExecutable(true, false);
lower.setReadable(true, false);
handler.flush();
archive.getListOfFiles(lower, fileList, null, deplLogger);
assertTrue(logRecords.isEmpty(),
"FileArchive was incorrectly unable to list files; error key in log record:" + logRecords);
}
private static class RecordingHandler extends Handler {
private final List<LogRecord> records = new ArrayList<>();
@Override
public void close() {
records.clear();
}
@Override
public void flush() {
records.clear();
}
@Override
public void publish(LogRecord record) {
records.add(record);
}
List<LogRecord> logRecords() {
return records;
}
}
}