/**
* Copyright (c) Codice Foundation
* <p>
* This 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 any later version.
* <p>
* 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. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package org.codice.ddf.catalog.content.monitor;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.component.mock.MockComponent;
import org.apache.camel.model.FromDefinition;
import org.apache.camel.model.ProcessorDefinition;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import ddf.catalog.Constants;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
@RunWith(JUnit4.class)
public class ContentDirectoryMonitorTest extends CamelTestSupport {
private static final String PROTOCOL = "file://";
private static final String DUMMY_DATA = "Dummy data in a text file. ";
private static final String[] ATTRIBUTE_OVERRIDES =
new String[] {"test1=someParameter1", "test1=someParameter0", "test2=(some,parameter,with,commas)"};
private static final int MAX_SECONDS_FOR_FILE_COPY = 5;
private static final int MAX_CHECKS_FOR_FILE_COPY = 10;
private String monitoredDirectoryPath;
private File monitoredDirectory;
private CamelContext camelContext;
private ContentDirectoryMonitor monitor;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void setup() throws Exception {
monitoredDirectory = temporaryFolder.newFolder("inbox");
monitoredDirectoryPath = monitoredDirectory.getCanonicalPath();
camelContext = super.createCamelContext();
camelContext.start();
MockComponent contentComponent = new MockComponent();
camelContext.addComponent("content", contentComponent);
monitor = createContentDirectoryMonitor();
monitor.setReadLockIntervalMilliseconds(1000);
monitor.setNumThreads(1);
}
@After
public void destroy() throws Exception {
monitor.destroy(0);
camelContext.stop();
}
@Test
public void testUpdateCallbackNullProperties() {
assertThat(monitor.getNumThreads(), is(1));
assertThat(monitor.getReadLockIntervalMilliseconds(), is(1000));
monitor.updateCallback(null);
assertThat(monitor.getNumThreads(), is(1));
assertThat(monitor.getReadLockIntervalMilliseconds(), is(1000));
}
@Test
public void testUpdateCallback() {
Map<String, Object> properties = new HashMap<>();
properties.put("numThreads", 2);
properties.put("readLockIntervalMilliseconds", 2000);
monitor.updateCallback(properties);
assertThat(monitor.getNumThreads(), is(2));
assertThat(monitor.getReadLockIntervalMilliseconds(), is(2000));
}
@Test
public void testRouteCreationWithoutContentComponent() throws Exception {
camelContext.removeComponent("content");
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.DELETE);
assertThat("The content directory monitor should not have any route definitions",
monitor.getRouteDefinitions(),
empty());
assertThat("The camel context should not have any route definitions",
camelContext.getRouteDefinitions(),
empty());
}
@Test
public void testRouteCreationWithCopyIngestedFiles() throws Exception {
testRouteCreationWithGivenCopyStatus(ContentDirectoryMonitor.MOVE);
}
@Test
public void testRouteCreationWithoutCopyIngestedFiles() throws Exception {
testRouteCreationWithGivenCopyStatus(ContentDirectoryMonitor.DELETE);
}
@Test
public void testRouteCreationWithKeepIngestedFiles() throws Exception {
testRouteCreationWithGivenCopyStatus(ContentDirectoryMonitor.IN_PLACE);
}
private void testRouteCreationWithGivenCopyStatus(String processingMechanism) throws Exception {
submitConfigOptions(monitor, monitoredDirectoryPath, processingMechanism);
assertThat("The content directory monitor should only have one route definition",
monitor.getRouteDefinitions(),
hasSize(1));
RouteDefinition routeDefinition = monitor.getRouteDefinitions()
.get(0);
verifyRoute(routeDefinition, monitoredDirectoryPath, processingMechanism);
}
@Test
public void testMoveFile() throws Exception {
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.MOVE);
doAndVerifyFileMove(monitoredDirectory, monitoredDirectory, "input1.txt");
}
@Test
public void testBlackListedFileExtension() throws Exception {
System.setProperty("bad.file.extensions", ".txt");
monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.MOVE);
doAndVerifyFileDidNotMove(monitoredDirectory, monitoredDirectory, "input1.txt");
System.setProperty("bad.file.extensions", "");
}
@Test
public void testBlackListedFile() throws Exception {
System.setProperty("bad.files", "input1.txt");
monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.MOVE);
doAndVerifyFileDidNotMove(monitoredDirectory, monitoredDirectory, "input1.txt");
System.setProperty("bad.files", "");
}
@Test
public void testBlackListedFileExtensions() throws Exception {
System.setProperty("bad.file.extensions", ".txt,.bin");
monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.MOVE);
doAndVerifyFileDidNotMove(monitoredDirectory, monitoredDirectory, "input1.txt");
doAndVerifyFileDidNotMove(monitoredDirectory, monitoredDirectory, "input1.bin");
System.setProperty("bad.file.extensions", "");
}
@Test
public void testBlackListedEmptyFileExtensions() throws Exception {
System.setProperty("bad.file.extensions", "");
monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor, monitoredDirectoryPath, ContentDirectoryMonitor.MOVE);
doAndVerifyFileMove(monitoredDirectory, monitoredDirectory, "input1.txt");
}
@Test
public void testUpdateExistingContentDirectoryMonitor() throws Exception {
File monitoredDirectory1 = temporaryFolder.newFolder("inbox1");
File monitoredDirectory2 = temporaryFolder.newFolder("inbox2");
submitConfigOptions(monitor,
monitoredDirectory1.getCanonicalPath(),
ContentDirectoryMonitor.MOVE);
doAndVerifyFileMove(monitoredDirectory1, monitoredDirectory1, "input1.txt");
submitConfigOptions(monitor,
monitoredDirectory2.getCanonicalPath(),
ContentDirectoryMonitor.MOVE);
doAndVerifyFileMove(monitoredDirectory2, monitoredDirectory2, "input2.txt");
doAndVerifyFileDidNotMove(monitoredDirectory1, monitoredDirectory2, "input3.txt");
}
@Test
public void testMultipleContentDirectoryMonitors() throws Exception {
File monitoredDirectory1 = temporaryFolder.newFolder("inbox1");
File monitoredDirectory2 = temporaryFolder.newFolder("inbox2");
ContentDirectoryMonitor monitor1 = createContentDirectoryMonitor();
ContentDirectoryMonitor monitor2 = createContentDirectoryMonitor();
submitConfigOptions(monitor1,
monitoredDirectory1.getCanonicalPath(),
ContentDirectoryMonitor.MOVE);
submitConfigOptions(monitor2,
monitoredDirectory2.getCanonicalPath(),
ContentDirectoryMonitor.MOVE);
doAndVerifyFileMove(monitoredDirectory1, monitoredDirectory1, "input1.txt");
doAndVerifyFileMove(monitoredDirectory2, monitoredDirectory2, "input2.txt");
}
@Test
public void testDirectoryMonitorWithParameters() throws Exception {
ContentDirectoryMonitor monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor,
monitoredDirectoryPath,
ContentDirectoryMonitor.MOVE,
ATTRIBUTE_OVERRIDES,
1,
1000);
RouteDefinition routeDefinition = camelContext.getRouteDefinitions()
.get(0);
assertThat(routeDefinition.toString(), containsString(
"SetHeader[" + Constants.ATTRIBUTE_OVERRIDES_KEY
+ ", {{test2=[(some,parameter,with,commas)], test1=[someParameter1, someParameter0]}}"));
}
@Test
public void testDirectoryMonitorThreadNumFallback() throws Exception {
ContentDirectoryMonitor monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor,
monitoredDirectoryPath,
ContentDirectoryMonitor.MOVE,
ATTRIBUTE_OVERRIDES,
16,
1000);
assertThat(monitor.getNumThreads(), is(8));
}
@Test
public void testDirectoryMonitorThreadNumMinimum() throws Exception {
ContentDirectoryMonitor monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor,
monitoredDirectoryPath,
ContentDirectoryMonitor.MOVE,
ATTRIBUTE_OVERRIDES,
0,
1000);
assertThat(monitor.getNumThreads(), is(1));
}
@Test
public void testDirectoryMonitorReadLockIntervalMinimum() throws Exception {
ContentDirectoryMonitor monitor = createContentDirectoryMonitor();
submitConfigOptions(monitor,
monitoredDirectoryPath,
ContentDirectoryMonitor.MOVE,
ATTRIBUTE_OVERRIDES,
1,
1);
assertThat(monitor.getReadLockIntervalMilliseconds(), is(100));
}
@Test
public void testRouteCreationMissingMonitoredDirectory() throws Exception {
submitConfigOptions(monitor, "", ContentDirectoryMonitor.MOVE);
assertThat("Camel context should not have any route definitions",
camelContext.getRouteDefinitions(),
empty());
assertThat("Content directory monitor should not have any route definitions",
monitor.getRouteDefinitions(),
empty());
}
private void doAndVerifyFileMove(File destinationFolder, File monitoredFolder,
String inputFileName) throws Exception {
doFileMove(destinationFolder, inputFileName);
Failsafe.with(new RetryPolicy().retryWhen(false)
.withMaxRetries(MAX_CHECKS_FOR_FILE_COPY)
.withDelay(5, TimeUnit.SECONDS))
.withFallback(() -> {
throw new RuntimeException("File did not get moved in time");
})
.get(() -> verifyFileMovedToIngestedDirectory(monitoredFolder, inputFileName));
assertThat("File SHOULD have been moved to the /.ingested directory",
verifyFileMovedToIngestedDirectory(monitoredFolder, inputFileName),
is(true));
}
private void doAndVerifyFileDidNotMove(File destinationFolder, File monitoredFolder,
String inputFileName) throws Exception {
doFileMove(destinationFolder, inputFileName);
TimeUnit.SECONDS.sleep(MAX_SECONDS_FOR_FILE_COPY);
assertThat("File SHOULD NOT have been moved to the /.ingested directory",
verifyFileMovedToIngestedDirectory(monitoredFolder, inputFileName),
is(false));
}
private void doFileMove(File destinationFolder, String inputFileName) throws Exception {
FileUtils.writeStringToFile(new File(destinationFolder, inputFileName), DUMMY_DATA);
template.sendBodyAndHeader(PROTOCOL + destinationFolder.getCanonicalPath(),
DUMMY_DATA,
Exchange.FILE_NAME,
inputFileName);
}
private boolean verifyFileMovedToIngestedDirectory(File monitoredFolder, String fileName)
throws Exception {
File target = new File(monitoredFolder.getCanonicalPath() + "/.ingested/" + fileName);
return target.exists();
}
private void verifyRoute(RouteDefinition routeDefinition, String monitoredDirectory,
String processingMechanism) {
List<FromDefinition> fromDefinitions = routeDefinition.getInputs();
assertThat(fromDefinitions.size(), is(1));
String uri = fromDefinitions.get(0)
.getUri();
String expectedUri = "file:" + monitoredDirectory
+ "?recursive=true&moveFailed=.errors&readLockMinLength=1&readLock=changed&readLockTimeout=2000&readLockCheckInterval=1000";
if (ContentDirectoryMonitor.DELETE.equals(processingMechanism)) {
expectedUri += "&delete=true";
} else if (ContentDirectoryMonitor.MOVE.equals(processingMechanism)) {
expectedUri += "&move=.ingested";
} else if (ContentDirectoryMonitor.IN_PLACE.equals(processingMechanism)) {
expectedUri = "durable:" + monitoredDirectory;
}
assertThat(uri, equalTo(expectedUri));
List<ProcessorDefinition<?>> processorDefinitions = routeDefinition.getOutputs();
if (ContentDirectoryMonitor.IN_PLACE.equals(processingMechanism)) {
assertThat(processorDefinitions.size(), is(2));
} else {
assertThat(processorDefinitions.size(), is(1));
}
}
private void submitConfigOptions(ContentDirectoryMonitor monitor, String monitoredDirectory,
String processingMechanism) throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put("monitoredDirectoryPath", monitoredDirectory);
properties.put("processingMechanism", processingMechanism);
properties.put("numThreads", 1);
properties.put("readLockIntervalMilliseconds", 1000);
monitor.updateCallback(properties);
}
private void submitConfigOptions(ContentDirectoryMonitor monitor, String monitoredDirectory,
String processingMechanism, String[] attributeOverrides, int numThreads,
int readLockIntervalMilliseconds) throws Exception {
Map<String, Object> properties = new HashMap<>();
properties.put("monitoredDirectoryPath", monitoredDirectory);
properties.put("processingMechanism", processingMechanism);
properties.put("attributeOverrides", attributeOverrides);
properties.put("numThreads", numThreads);
properties.put("readLockIntervalMilliseconds", readLockIntervalMilliseconds);
monitor.updateCallback(properties);
}
private ContentDirectoryMonitor createContentDirectoryMonitor() {
ContentDirectoryMonitor monitor = new ContentDirectoryMonitor(camelContext,
1,
1,
Runnable::run);
monitor.systemSubjectBinder = exchange -> {
};
monitor.setNumThreads(1);
monitor.setReadLockIntervalMilliseconds(1000);
return monitor;
}
}