/** * 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.commands.catalog; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.karaf.shell.api.action.Argument; import org.apache.karaf.shell.api.action.Command; import org.apache.karaf.shell.api.action.Option; import org.apache.karaf.shell.api.action.lifecycle.Service; import org.codice.ddf.commands.catalog.facade.CatalogFacade; import org.joda.time.Period; import org.joda.time.format.PeriodFormatter; import org.joda.time.format.PeriodFormatterBuilder; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.Constants; import ddf.catalog.data.BinaryContent; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.data.impl.MetacardImpl; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transform.MetacardTransformer; import ddf.catalog.transform.QueryResponseTransformer; import ddf.security.common.audit.SecurityLogger; @Service @Command(scope = CatalogCommands.NAMESPACE, name = "dump", description = "Exports Metacards from the current Catalog. Does not remove them.") public class DumpCommand extends CqlCommands { public static final String FILE_PATH = "filePath"; private static final Logger LOGGER = LoggerFactory.getLogger(DumpCommand.class); private static final String ZIP_COMPRESSION = "zipCompression"; private static List<MetacardTransformer> transformers = null; private final PeriodFormatter timeFormatter = new PeriodFormatterBuilder().printZeroRarelyLast() .appendDays() .appendSuffix(" day", " days") .appendSeparator(" ") .appendHours() .appendSuffix(" hour", " hours") .appendSeparator(" ") .appendMinutes() .appendSuffix(" minute", " minutes") .appendSeparator(" ") .appendSeconds() .appendSuffix(" second", " seconds") .toFormatter(); @Argument(name = "Dump directory path", description = "Directory to export Metacards into. Paths are absolute and must be in quotes. Files in directory will be overwritten if they already exist.", index = 0, multiValued = false, required = true) String dirPath = null; @Argument(name = "Batch size", description = "Number of Metacards to retrieve and export at a time until completion. Change this argument based on system memory and CatalogProvider limits.", index = 1, multiValued = false, required = false) int pageSize = 1000; // DDF-535: remove "Transformer" alias in DDF 3.0 @Option(name = "--transformer", required = false, aliases = {"-t", "Transformer"}, multiValued = false, description = "The metacard transformer ID to use to transform metacards into data files. " + "The default metacard transformer is the XML transformer.") String transformerId = DEFAULT_TRANSFORMER_ID; // DDF-535: remove "Extension" alias in DDF 3.0 @Option(name = "--extension", required = false, aliases = {"-e", "Extension"}, multiValued = false, description = "The file extension of the data files.") String fileExtension = null; @Option(name = "--multithreaded", required = false, aliases = {"-m", "Multithreaded"}, multiValued = false, description = "Number of threads to use when dumping. Setting " + "this value too high for your system can cause performance degradation.") int multithreaded = 20; @Option(name = "--dirlevel", required = false, multiValued = false, description = "Number of subdirectory levels to create. Two characters from the ID " + "will be used to name each subdirectory level.") int dirLevel = 0; @Option(name = "--include-content", required = false, aliases = {}, multiValued = false, description = "Dump the entire Catalog and local content into a zip file with the specified name using the default transformer.") String zipFileName; private Map<String, Serializable> zipArgs; @Override protected Object executeWithSubject() throws Exception { if (FilenameUtils.getExtension(dirPath) .equals("") && !dirPath.endsWith(File.separator)) { dirPath += File.separator; } final File dumpDir = new File(dirPath); if (!dumpDir.exists()) { printErrorMessage("Directory [" + dirPath + "] must exist."); console.println("If the directory does indeed exist, try putting the path in quotes."); return null; } if (!dumpDir.isDirectory()) { printErrorMessage("Path [" + dirPath + "] must be a directory."); return null; } if (!SERIALIZED_OBJECT_ID.matches(transformerId)) { transformers = getTransformers(); if (transformers == null) { console.println(transformerId + " is an invalid metacard transformer."); return null; } } if (StringUtils.isNotBlank(zipFileName) && new File(dirPath + zipFileName).exists()) { console.println("Cannot dump Catalog. Zip file " + zipFileName + " already exists."); return null; } SecurityLogger.audit("Called catalog:dump command with path : {}", dirPath); CatalogFacade catalog = getCatalog(); if (StringUtils.isNotBlank(zipFileName)) { zipArgs = new HashMap<>(); zipArgs.put(FILE_PATH, dirPath + zipFileName); } QueryImpl query = new QueryImpl(getFilter()); query.setRequestsTotalResultsCount(false); query.setPageSize(pageSize); Map<String, Serializable> props = new HashMap<>(); // Avoid caching all results while dumping with native query mode props.put("mode", "native"); final AtomicLong resultCount = new AtomicLong(0); long start = System.currentTimeMillis(); SourceResponse response = catalog.query(new QueryRequestImpl(query, props)); BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(multithreaded); RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy(); final ExecutorService executorService = new ThreadPoolExecutor(multithreaded, multithreaded, 0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler); while (response.getResults() .size() > 0) { response = catalog.query(new QueryRequestImpl(query, props)); if (StringUtils.isNotBlank(zipFileName)) { try { Optional<QueryResponseTransformer> zipCompression = getZipCompression(); if (zipCompression.isPresent()) { BinaryContent binaryContent = zipCompression.get() .transform(response, zipArgs); if (binaryContent != null) { IOUtils.closeQuietly(binaryContent.getInputStream()); } Long resultSize = (long) response.getResults() .size(); printStatus(resultCount.addAndGet(resultSize)); } } catch (InvalidSyntaxException e) { LOGGER.info("No Zip Transformer found. Unable export metacards to a zip file."); } } else if (multithreaded > 1) { final List<Result> results = new ArrayList<>(response.getResults()); executorService.submit(() -> { boolean transformationFailed = false; for (final Result result : results) { Metacard metacard = result.getMetacard(); try { exportMetacard(dumpDir, metacard); } catch (IOException | CatalogTransformerException e) { transformationFailed = true; LOGGER.debug("Failed to dump metacard {}", metacard.getId(), e); executorService.shutdownNow(); } printStatus(resultCount.incrementAndGet()); } if (transformationFailed) { LOGGER.info( "One or more metacards failed to transform. Enable debug log for more details."); } }); } else { for (final Result result : response.getResults()) { Metacard metacard = result.getMetacard(); exportMetacard(dumpDir, metacard); printStatus(resultCount.incrementAndGet()); } } if (response.getResults() .size() < pageSize || pageSize == -1) { break; } if (pageSize > 0) { query.setStartIndex(query.getStartIndex() + pageSize); } } executorService.shutdown(); while (!executorService.isTerminated()) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { // ignore } } long end = System.currentTimeMillis(); String elapsedTime = timeFormatter.print(new Period(start, end).withMillis(0)); console.printf(" %d file(s) dumped in %s\t%n", resultCount.get(), elapsedTime); LOGGER.debug("{} file(s) dumped in {}", resultCount.get(), elapsedTime); console.println(); SecurityLogger.audit("Exported {} files to {}", resultCount.get(), dirPath); return null; } private void exportMetacard(File dumpLocation, Metacard metacard) throws IOException, CatalogTransformerException { if (SERIALIZED_OBJECT_ID.matches(transformerId)) { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(getOutputFile( dumpLocation, metacard)))) { oos.writeObject(new MetacardImpl(metacard)); oos.flush(); } } else { BinaryContent binaryContent; if (metacard != null) { try (FileOutputStream fos = new FileOutputStream(getOutputFile(dumpLocation, metacard))) { for (MetacardTransformer transformer : transformers) { binaryContent = transformer.transform(metacard, new HashMap<>()); if (binaryContent != null) { fos.write(binaryContent.getByteArray()); break; } } } } } } private File getOutputFile(File dumpLocation, Metacard metacard) throws IOException { String extension = ""; if (fileExtension != null) { extension = "." + fileExtension; } String id = metacard.getId(); File parent = dumpLocation; if (dirLevel > 0 && id.length() >= dirLevel * 2) { for (int i = 0; i < dirLevel; i++) { parent = new File(parent, id.substring(i * 2, i * 2 + 2)); } FileUtils.forceMkdir(parent); } return new File(parent, id + extension); } protected void printStatus(long count) { console.print(String.format(" %d file(s) dumped\t\r", count)); console.flush(); } private List<MetacardTransformer> getTransformers() { ServiceReference[] refs = null; try { refs = bundleContext.getAllServiceReferences(MetacardTransformer.class.getName(), "(|" + "(" + Constants.SERVICE_ID + "=" + transformerId + ")" + ")"); } catch (InvalidSyntaxException e) { console.printf("Fail to get MetacardTransformer references due to %s", e.getMessage()); } if (refs == null || refs.length == 0) { return null; } List<MetacardTransformer> metacardTransformerList = new ArrayList<>(); for (ServiceReference ref : refs) { metacardTransformerList.add((MetacardTransformer) bundleContext.getService(ref)); } return metacardTransformerList; } private Optional<QueryResponseTransformer> getZipCompression() throws InvalidSyntaxException { return getServiceByFilter(QueryResponseTransformer.class, "(|" + "(" + Constants.SERVICE_ID + "=" + ZIP_COMPRESSION + ")" + ")"); } }