/**
* 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 ddf.content.provider.filesystem;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.activation.MimeType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.slf4j.ext.XLogger;
import ddf.content.data.ContentItem;
import ddf.content.operation.CreateRequest;
import ddf.content.operation.CreateResponse;
import ddf.content.operation.DeleteRequest;
import ddf.content.operation.DeleteResponse;
import ddf.content.operation.ReadRequest;
import ddf.content.operation.ReadResponse;
import ddf.content.operation.UpdateRequest;
import ddf.content.operation.UpdateResponse;
import ddf.content.operation.impl.CreateResponseImpl;
import ddf.content.operation.impl.DeleteResponseImpl;
import ddf.content.operation.impl.ReadResponseImpl;
import ddf.content.operation.impl.UpdateResponseImpl;
import ddf.content.storage.StorageException;
import ddf.content.storage.StorageProvider;
import ddf.mime.MimeTypeMapper;
/**
* The File System Provider provides the implementation to create/update/delete content items as
* files in the DDF Content Repository. The File System Provider is an implementation of the
* {@link StorageProvider} interface. When installed, it is invoked by the @link{ContentFramework} to
* create, update, or delete a file in the DDF Content Repository, which is
* located in the <code><DDF_INSTALL_DIR>/content/store directory</code>.
*/
public class FileSystemProvider implements StorageProvider {
public static final String CONTENT_URI_PREFIX = "content:";
public static final String DEFAULT_CONTENT_REPOSITORY = "content" + File.separator + "store";
private static final String DEFAULT_MIME_TYPE = "application/octet-stream";
/**
* Optional id parameter for mime type, e.g., application/json;id=geojson
*/
private static final String ID_PARAMETER = "id";
private static final XLogger LOGGER = new XLogger(LoggerFactory.getLogger(FileSystemProvider.class));
/**
* Mapper for file extensions-to-mime types (and vice versa)
*/
private MimeTypeMapper mimeTypeMapper;
/**
* Root directory for entire content repository
*/
private String baseContentDirectory;
/**
* Default constructor, invoked by blueprint.
*/
public FileSystemProvider() {
LOGGER.info("File System Provider initializing...");
}
@Override
public CreateResponse create(CreateRequest createRequest) throws StorageException {
LOGGER.trace("ENTERING: create");
ContentItem item = createRequest.getContentItem();
ContentItem createdItem = null;
try {
// Create the root directory for entire content repository if does not
// already exist.
File parentDir = new File(baseContentDirectory);
if (!parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("Error creating content file system root directory");
}
createdItem = generateContentFile(item);
} catch (IOException e) {
throw new StorageException(e);
}
CreateResponse response = new CreateResponseImpl(createRequest, createdItem);
LOGGER.trace("EXITING: create");
return response;
}
@Override
public ReadResponse read(ReadRequest readRequest) throws StorageException {
LOGGER.trace("ENTERING: read");
String id = readRequest.getId();
File file = getFileForContentId(id);
LOGGER.debug("Reading file {} from directory {}", file.getName(), id);
String extension = FilenameUtils.getExtension(file.getName());
String mimeType = DEFAULT_MIME_TYPE;
try {
mimeType = mimeTypeMapper.guessMimeType(new FileInputStream(file), extension);
} catch (Exception e) {
LOGGER.warn("Could not determine mime type for file extension = {}; defaulting to {}",
extension, DEFAULT_MIME_TYPE);
}
LOGGER.debug("mimeType = {}", mimeType);
ContentFile returnItem = new ContentFile(file, id, mimeType);
ReadResponse response = new ReadResponseImpl(readRequest, returnItem);
LOGGER.trace("EXITING: read");
return response;
}
@Override
public UpdateResponse update(UpdateRequest updateRequest) throws StorageException {
LOGGER.trace("ENTERING: update");
ContentItem item = updateRequest.getContentItem();
ContentItem updatedItem = null;
LOGGER.debug("Updating item with id = {}", item.getId());
try {
updatedItem = updateContentFile(item);
} catch (IOException e) {
throw new StorageException(e);
}
UpdateResponse response = new UpdateResponseImpl(updateRequest, updatedItem);
LOGGER.trace("EXITING: update");
return response;
}
@Override
public DeleteResponse delete(DeleteRequest deleteRequest) throws StorageException {
LOGGER.trace("ENTERING: delete");
ContentItem itemToBeDeleted = deleteRequest.getContentItem();
String id = itemToBeDeleted.getId();
boolean isDeleted = false;
LOGGER.debug("File to be deleted: {}", id);
File fileToBeDeleted = getFileForContentId(id);
if (!fileToBeDeleted.exists()) {
throw new StorageException("File doesn't exist for id: " + id);
}
ContentItem deletedContentItem = null;
if (!fileToBeDeleted.isDirectory()) {
isDeleted = fileToBeDeleted.delete();
if (!isDeleted) {
throw new StorageException("Could not delete file: " + id);
} else {
// Delete parent directory (identified by contentId) since it is now empty
// (always only one file per GUID directory) and will never be used again.
File dirToBeDeleted = getDirectoryForContentId(id);
try {
FileUtils.deleteDirectory(dirToBeDeleted);
} catch (IOException e) {
LOGGER.info("Unable to delete directory {} for id = {}",
dirToBeDeleted.getAbsolutePath(), id);
}
deletedContentItem = new ContentFile(null, id,
itemToBeDeleted.getMimeTypeRawData());
String contentUri = CONTENT_URI_PREFIX + deletedContentItem.getId();
LOGGER.debug("contentUri = {}", contentUri);
deletedContentItem.setUri(contentUri);
}
} else {
throw new StorageException("Invalid ID. Cannot delete directory.");
}
DeleteResponse response = new DeleteResponseImpl(deleteRequest, deletedContentItem,
isDeleted);
LOGGER.trace("EXITING: delete");
return response;
}
private ContentItem generateContentFile(ContentItem item) throws IOException, StorageException {
LOGGER.trace("ENTERING: generateContentFile");
String mimeType = getMimeType(item.getMimeType());
String fileId = item.getId();
if (StringUtils.isNotEmpty(item.getFilename())) {
fileId += File.separator + item.getFilename();
}
LOGGER.debug(
"itemId = " + item.getId() + ", mimeType = " + mimeType + ", itemFilename = "
+ item.getFilename());
LOGGER.debug("fileId = {}", fileId);
File createdFile = createFile(fileId);
FileUtils.copyInputStreamToFile(item.getInputStream(), createdFile);
ContentItem contentItem = new ContentFile(createdFile, item.getId(),
item.getMimeTypeRawData(), item.getFilename());
String contentUri = CONTENT_URI_PREFIX + contentItem.getId();
LOGGER.debug("contentUri = {}", contentUri);
contentItem.setUri(contentUri);
LOGGER.trace("EXITING: generateContentFile");
return contentItem;
}
private ContentItem updateContentFile(ContentItem item) throws IOException, StorageException {
LOGGER.trace("ENTERING: updateContentFile");
String fileId = item.getId();
LOGGER.debug("File ID: {}", fileId);
File fileToUpdate = getFileForContentId(fileId);
ContentItem contentItem = null;
if (fileToUpdate.exists()) {
FileUtils.copyInputStreamToFile(item.getInputStream(), fileToUpdate);
contentItem = new ContentFile(fileToUpdate, item.getId(), item.getMimeTypeRawData());
String contentUri = CONTENT_URI_PREFIX + contentItem.getId();
LOGGER.debug("contentUri = {}", contentUri);
contentItem.setUri(contentUri);
LOGGER.debug("updated file length = " + contentItem.getSize());
} else {
String msg = "Unable to update - Content Item does not exist with id " + item.getId()
+ " (fileId = " + fileId + ")";
LOGGER.debug(msg);
throw new StorageException(msg);
}
LOGGER.trace("EXITING: updateContentFile");
return contentItem;
}
private File createFile(final String newFileID) throws IOException {
LOGGER.trace("ENTERING: createFile");
File file = getFileFromContentRepository(newFileID);
if (file == null) {
throw new IOException("Error getting file: " + newFileID);
}
// create directories
File directory = file.getParentFile();
if (directory == null || (!directory.exists() && !directory.mkdirs())) {
throw new IOException("Error creating directory structure to save file.");
}
// create file and write to it, if it doesn't already exist
if (!file.exists() && !file.createNewFile()) {
throw new IOException("Error creating file: " + file.getAbsolutePath());
}
LOGGER.trace("EXITING: createFile");
return file;
}
private File getFileFromContentRepository(String id) {
LOGGER.trace("ENTERING: getFileFromContentRepository");
LOGGER.debug("id = {}", id);
File baseURIFile = null;
if (StringUtils.isNotEmpty(id)) {
// Normalize and concatenate the paths
String filepath = FilenameUtils.concat(baseContentDirectory, removeSlashPrefix(id));
LOGGER.debug("Full filepath = {}", filepath);
baseURIFile = new File(filepath);
} else {
LOGGER.warn(
"Could not obtain reference to content item. Possibly invalid content id: {}",
id);
}
LOGGER.trace("EXITING: getFileFromContentRepository");
return baseURIFile;
}
private String removeSlashPrefix(final String path) {
String newPath = path;
char firstChar = path.charAt(0);
if (firstChar == '/' || firstChar == '\\') {
newPath = path.substring(1);
}
return newPath;
}
public MimeTypeMapper getMimeTypeMapper() {
return mimeTypeMapper;
}
public void setMimeTypeMapper(MimeTypeMapper mimeTypeMapper) {
this.mimeTypeMapper = mimeTypeMapper;
}
public String getBaseContentDirectory() {
return baseContentDirectory;
}
public void setBaseContentDirectory(final String baseDirectory) {
String newBaseDir = "";
if (!baseDirectory.isEmpty()) {
String path = FilenameUtils.normalize(baseDirectory);
File directory = new File(path);
// Create the directory if it doesn't exist
if ((!directory.exists() && directory.mkdirs()) || (directory.isDirectory() && directory
.canRead())) {
LOGGER.info("Setting base content directory to: {}", path);
newBaseDir = path;
}
}
// if invalid baseDirectory was provided or baseDirectory is
// an empty string, default to the DEFAULT_CONTENT_REPOSITORY in <karaf.home>
if (newBaseDir.isEmpty()) {
try {
final File karafHomeDir = new File(System.getProperty("karaf.home"));
if (karafHomeDir.isDirectory()) {
final File fspDir = new File(
karafHomeDir + File.separator + DEFAULT_CONTENT_REPOSITORY);
// if directory does not exist, try to create it
if (fspDir.isDirectory() || fspDir.mkdirs()) {
LOGGER.info("Setting base content directory to: {}",
fspDir.getAbsolutePath());
newBaseDir = fspDir.getAbsolutePath();
} else {
LOGGER.warn(
"Unable to create FileSystemProvider folder: {}. Please check that DDF has permissions to create this folder. Using default folder.",
fspDir.getAbsolutePath());
}
} else {
LOGGER.warn(
"Karaf home folder defined by system property karaf.home is not a directory. Using default folder.");
}
} catch (NullPointerException npe) {
LOGGER.warn(
"Unable to create FileSystemProvider folder - karaf.home system property not defined. Using default folder.");
}
}
this.baseContentDirectory = newBaseDir;
LOGGER.debug("Set base content directory to: {}", this.baseContentDirectory);
}
private String getMimeType(MimeType mimeType) {
LOGGER.trace("ENTERING: getMimeType");
String mimeTypeStr = mimeType.getBaseType();
String mimeTypeIdValue = mimeType.getParameter(ID_PARAMETER);
if (StringUtils.isNotEmpty(mimeTypeIdValue)) {
mimeTypeStr += ";id=" + mimeTypeIdValue;
}
LOGGER.debug("mimeTypeStr = {}", mimeTypeStr);
LOGGER.trace("EXITING: getMimeType");
return mimeTypeStr;
}
private File getDirectoryForContentId(String contentId) throws StorageException {
LOGGER.trace("ENTERING: getDirectoryForContentId");
LOGGER.debug("contentId = {}", contentId);
File dir = getFileFromContentRepository(contentId);
if (dir == null || !dir.exists() || !dir.isDirectory()) {
throw new StorageException(
"Directory does not exist in content repository with id = " + contentId);
}
LOGGER.trace("EXITING: getDirectoryForContentId");
return dir;
}
private File getFileForContentId(String contentId) throws StorageException {
LOGGER.trace("ENTERING: getFileFromContentId");
LOGGER.debug("contentId = {}", contentId);
File dir = getDirectoryForContentId(contentId);
File[] files = dir.listFiles();
if (files == null || files.length == 0) {
throw new StorageException("No files in directory " + contentId);
} else if (files.length > 1) {
throw new StorageException("More than one file in directory " + contentId
+ " - cannot determine which file to work on.");
}
LOGGER.trace("EXITING: getFileFromContentId");
return files[0];
}
}