package com.marklogic.client.modulesloader.impl;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.marklogic.client.DatabaseClient;
import com.marklogic.client.document.DocumentDescriptor;
import com.marklogic.client.document.DocumentWriteSet;
import com.marklogic.client.document.GenericDocumentManager;
import com.marklogic.client.helper.LoggingObject;
import com.marklogic.client.io.DocumentMetadataHandle;
import com.marklogic.client.io.FileHandle;
import com.marklogic.client.modulesloader.ModulesManager;
import com.marklogic.client.modulesloader.xcc.DefaultDocumentFormatGetter;
/**
* Uses the /v1/documents endpoint in a REST API to load asset modules. This is slower than XccAssetLoader, but it has
* the advantage of not needing any other permissions other than what's required by the /v1/documents endpoint in the
* REST API.
* <p>
* Note that the DatabaseClient that this class needs must be configured to point to your modules database, not your
* content database. That's because /v1/documents can only ingest into the database associated with the REST API
* connection.
*/
public class RestApiAssetLoader extends LoggingObject implements FileVisitor<Path> {
// Controls what files/directories are processed
private FileFilter fileFilter = new AssetFileFilter();
// Default permissions and collections for each module
private String permissions = "rest-admin,read,rest-admin,update,rest-extension-user,execute";
private String[] collections;
private DocumentPermissionsParser documentPermissionsParser = new DefaultDocumentPermissionsParser();
private FormatGetter formatGetter = new DefaultDocumentFormatGetter();
// State that is maintained while visiting each asset path. Would need to move this to another class if this
// class ever needs to be thread-safe.
private Path currentAssetPath;
private Path currentRootPath;
private Set<File> filesLoaded;
private ModulesManager modulesManager;
private DocumentWriteSet writeSet;
private GenericDocumentManager docManager;
private int writeCount = 0;
private int batchSize = 100;
public RestApiAssetLoader(DatabaseClient client) {
this.docManager = client.newDocumentManager();
}
/**
* For walking one or many paths and loading modules in each of them.
*/
public Set<File> loadAssets(String... paths) {
filesLoaded = new HashSet<>();
try {
for (String path : paths) {
if (logger.isDebugEnabled()) {
logger.debug(format("Loading assets from path: %s", path));
}
this.currentAssetPath = Paths.get(path);
this.currentRootPath = this.currentAssetPath;
try {
Files.walkFileTree(this.currentAssetPath, this);
} catch (IOException ie) {
throw new RuntimeException(format("Error while walking assets file tree: %s", ie.getMessage()), ie);
}
}
return filesLoaded;
} finally {
// Write anything that hasn't been flushed yet
if (writeSet != null && writeCount > 0) {
if (logger.isInfoEnabled()) {
logger.info("Writing write set");
}
docManager.write(writeSet);
}
writeCount = 0;
}
}
/**
* Loads a file into the internally held DocumentWriteSet. If the writeCount is the batchSize or greater, than the
* writeSet is written.
*
* @param uri
* @param f
*/
public void loadFile(String uri, File f) {
if (modulesManager != null && !modulesManager.hasFileBeenModifiedSinceLastInstalled(f)) {
return;
}
DocumentDescriptor descriptor = docManager.newDescriptor(uri);
descriptor.setFormat(formatGetter.getFormat(f));
DocumentMetadataHandle metadataHandle = new DocumentMetadataHandle();
if (this.collections != null) {
metadataHandle.getCollections().addAll(collections);
}
if (this.permissions != null && this.documentPermissionsParser != null) {
this.documentPermissionsParser.parsePermissions(this.permissions, metadataHandle.getPermissions());
}
if (logger.isInfoEnabled()) {
logger.info(format("Loading module with URI: %s", uri));
}
FileHandle fileHandle = new FileHandle(f);
if (writeSet == null) {
writeSet = docManager.newWriteSet();
writeSet.add(descriptor, metadataHandle, fileHandle);
writeCount++;
} else if (writeCount >= batchSize) {
if (logger.isInfoEnabled()) {
logger.info("Writing write set");
}
docManager.write(writeSet);
writeCount = 0;
} else {
writeSet.add(descriptor, metadataHandle, fileHandle);
writeCount++;
}
if (modulesManager != null) {
modulesManager.saveLastInstalledTimestamp(f, new Date());
}
}
/**
* FileVisitor method that determines if we should visit the directory or not via the fileFilter.
*/
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attributes) throws IOException {
boolean accept = fileFilter.accept(path.toFile());
if (accept) {
if (logger.isDebugEnabled()) {
logger.debug("Visiting directory: " + path);
}
return FileVisitResult.CONTINUE;
} else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping directory: " + path);
}
return FileVisitResult.SKIP_SUBTREE;
}
}
/**
* FileVisitor method that loads the file into the modules database if the fileFilter accepts it.
*/
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attributes) throws IOException {
if (fileFilter.accept(path.toFile())) {
Path relPath = currentAssetPath.relativize(path);
String uri = "/" + relPath.toString().replace("\\", "/");
if (this.currentRootPath != null) {
String name = this.currentRootPath.toFile().getName();
// A bit of a hack to support the special "root" directory.
if (!"root".equals(name)) {
uri = "/" + name + uri;
}
}
loadFile(uri, path.toFile());
filesLoaded.add(path.toFile());
}
return FileVisitResult.CONTINUE;
}
/**
* A bit of a hack so that any modules in the samplestack-inspired "ext" directory have "/ext" prepended to their
* URI.
*
* @return
*/
// protected boolean isNotRootAssetsPath() {
// return this.currentRootPath != null && this.currentRootPath.toFile().getName().equals("root");
// }
@Override
public FileVisitResult postVisitDirectory(Path path, IOException exception) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path path, IOException exception) throws IOException {
return FileVisitResult.CONTINUE;
}
public void setPermissions(String permissions) {
this.permissions = permissions;
}
public void setCollections(String[] collections) {
this.collections = collections;
}
public void setFormatGetter(FormatGetter formatGetter) {
this.formatGetter = formatGetter;
}
public void setModulesManager(ModulesManager modulesManager) {
this.modulesManager = modulesManager;
}
public void setFileFilter(FileFilter fileFilter) {
this.fileFilter = fileFilter;
}
public void setDocumentPermissionsParser(DocumentPermissionsParser documentPermissionsParser) {
this.documentPermissionsParser = documentPermissionsParser;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
}