diff --git a/pom.xml b/pom.xml
index 2d069357..0c467092 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,7 @@
com.kingsrook.qqqqqq-backend-core
- 0.0.0-20220623.214704-10
+ 0.0.0-20220628.161829-14
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java
index 9a7578a4..3051e18a 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/FilesystemBackendModuleInterface.java
@@ -22,30 +22,18 @@
package com.kingsrook.qqq.backend.module.filesystem.base;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
-import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
+import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
/*******************************************************************************
** Interface to add additional functionality commonly among the various filesystem
** module implementations.
*******************************************************************************/
-public interface FilesystemBackendModuleInterface
+public interface FilesystemBackendModuleInterface
{
/*******************************************************************************
- ** In contrast with the DeleteAction, which deletes RECORDS - this is a
- ** filesystem-(or s3, sftp, etc)-specific extension to delete an entire FILE
- ** e.g., for post-ETL.
- **
- ** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
+ ** For filesystem backends, get the module-specific action base-class, that helps
+ ** with functions like listing and deleting files.
*******************************************************************************/
- void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException;
-
- /*******************************************************************************
- ** Move a file from a source path, to a destination path.
- **
- ** @throws FilesystemException if the move is known to have failed
- *******************************************************************************/
- void moveFile(QInstance instance, QTableMetaData table, String source, String destination) throws FilesystemException;
+ AbstractBaseFilesystemAction getActionBase();
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java
index b8e07308..1df77c93 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/actions/AbstractBaseFilesystemAction.java
@@ -27,27 +27,42 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter;
import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractQTableRequest;
import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest;
import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableBackendDetails;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemBackendMetaData;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.AbstractFilesystemTableBackendDetails;
+import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.NotImplementedException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Base class for all Filesystem actions across all modules.
+ **
+ ** @param FILE The class that represents a file in the sub-module. e.g.,
+ * a java.io.File, or an S3Object.
*******************************************************************************/
public abstract class AbstractBaseFilesystemAction
{
+ private static final Logger LOG = LogManager.getLogger(AbstractBaseFilesystemAction.class);
+
+
/*******************************************************************************
** List the files for a table - to be implemented in module-specific subclasses.
@@ -60,31 +75,79 @@ public abstract class AbstractBaseFilesystemAction
public abstract InputStream readFile(FILE file) throws IOException;
/*******************************************************************************
- ** Add backend details to records about the file that they are in.
+ ** Write a file - to be implemented in module-specific subclasses.
*******************************************************************************/
- protected abstract void addBackendDetailsToRecords(List recordsInFile, FILE file);
+ public abstract void writeFile(QBackendMetaData backend, String path, byte[] contents) throws IOException;
+
+ /*******************************************************************************
+ ** Get a string that represents the full path to a file.
+ *******************************************************************************/
+ protected abstract String getFullPathForFile(FILE file);
+
+ /*******************************************************************************
+ ** In contrast with the DeleteAction, which deletes RECORDS - this is a
+ ** filesystem-(or s3, sftp, etc)-specific extension to delete an entire FILE
+ ** e.g., for post-ETL.
+ **
+ ** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
+ *******************************************************************************/
+ public abstract void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException;
+
+ /*******************************************************************************
+ ** Move a file from a source path, to a destination path.
+ **
+ ** @throws FilesystemException if the move is known to have failed
+ *******************************************************************************/
+ public abstract void moveFile(QInstance instance, QTableMetaData table, String source, String destination) throws FilesystemException;
+
+ /*******************************************************************************
+ ** e.g., with a base path of /foo/
+ ** and a table path of /bar/
+ ** and a file at /foo/bar/baz.txt
+ ** give us just the baz.txt part.
+ *******************************************************************************/
+ public abstract String stripBackendAndTableBasePathsFromFileName(FILE file, QBackendMetaData sourceBackend, QTableMetaData sourceTable);
+
/*******************************************************************************
- ** Append together the backend's base path (if present), with a table's path (again, if present).
+ ** Append together the backend's base path (if present), with a table's base
+ ** path (again, if present).
*******************************************************************************/
- protected String getFullPath(QTableMetaData table, QBackendMetaData backendBase)
+ public String getFullBasePath(QTableMetaData table, QBackendMetaData backendBase)
{
AbstractFilesystemBackendMetaData metaData = getBackendMetaData(AbstractFilesystemBackendMetaData.class, backendBase);
String fullPath = StringUtils.hasContent(metaData.getBasePath()) ? metaData.getBasePath() : "";
AbstractFilesystemTableBackendDetails tableDetails = getTableBackendDetails(AbstractFilesystemTableBackendDetails.class, table);
- if(StringUtils.hasContent(tableDetails.getPath()))
+ if(StringUtils.hasContent(tableDetails.getBasePath()))
{
- fullPath += File.separatorChar + tableDetails.getPath();
+ fullPath += File.separatorChar + tableDetails.getBasePath();
}
fullPath += File.separatorChar;
+ fullPath = stripDuplicatedSlashes(fullPath);
+
return fullPath;
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String stripDuplicatedSlashes(String path)
+ {
+ if(path == null)
+ {
+ return (null);
+ }
+
+ return (path.replaceAll("//+", "/"));
+ }
+
+
+
/*******************************************************************************
** Get the backend metaData, type-checked as the requested type.
*******************************************************************************/
@@ -119,6 +182,8 @@ public abstract class AbstractBaseFilesystemAction
*******************************************************************************/
public QueryResult executeQuery(QueryRequest queryRequest) throws QException
{
+ preAction(queryRequest);
+
try
{
QueryResult rs = new QueryResult();
@@ -131,20 +196,25 @@ public abstract class AbstractBaseFilesystemAction
for(FILE file : files)
{
+ LOG.info("Processing file: " + getFullPathForFile(file));
switch(tableDetails.getRecordFormat())
{
- case "csv":
+ case CSV:
{
- String fileContents = IOUtils.toString(readFile(file));
+ String fileContents = IOUtils.toString(readFile(file));
+ fileContents = customizeFileContentsAfterReading(table, fileContents);
+
List recordsInFile = new CsvToQRecordAdapter().buildRecordsFromCsv(fileContents, table, null);
addBackendDetailsToRecords(recordsInFile, file);
records.addAll(recordsInFile);
break;
}
- case "json":
+ case JSON:
{
- String fileContents = IOUtils.toString(readFile(file));
+ String fileContents = IOUtils.toString(readFile(file));
+ fileContents = customizeFileContentsAfterReading(table, fileContents);
+
List recordsInFile = new JsonToQRecordAdapter().buildRecordsFromJson(fileContents, table, null);
addBackendDetailsToRecords(recordsInFile, file);
@@ -162,8 +232,64 @@ public abstract class AbstractBaseFilesystemAction
}
catch(Exception e)
{
- e.printStackTrace();
+ LOG.warn("Error executing query", e);
throw new QException("Error executing query", e);
}
}
+
+
+
+ /*******************************************************************************
+ ** Add backend details to records about the file that they are in.
+ *******************************************************************************/
+ protected void addBackendDetailsToRecords(List recordsInFile, FILE file)
+ {
+ recordsInFile.forEach(record ->
+ {
+ record.withBackendDetail(FilesystemRecordBackendDetailFields.FULL_PATH, getFullPathForFile(file));
+ });
+ }
+
+
+
+ /*******************************************************************************
+ ** Method that subclasses can override to add pre-action things (e.g., setting up
+ ** s3 client).
+ *******************************************************************************/
+ protected void preAction(AbstractQTableRequest tableRequest)
+ {
+ /////////////////////////////////////////////////////////////////////
+ // noop in base class - subclasses can add functionality if needed //
+ /////////////////////////////////////////////////////////////////////
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String customizeFileContentsAfterReading(QTableMetaData table, String fileContents) throws QException
+ {
+ Optional optionalCustomizer = table.getCustomizer("postFileRead");
+ if(optionalCustomizer.isEmpty())
+ {
+ return (fileContents);
+ }
+ QCodeReference customizer = optionalCustomizer.get();
+
+ try
+ {
+ Class> customizerClass = Class.forName(customizer.getName());
+
+ @SuppressWarnings("unchecked")
+ Function function = (Function) customizerClass.getConstructor().newInstance();
+
+ return function.apply(fileContents);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error customizing file contents", e));
+ }
+ }
+
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/AbstractFilesystemTableBackendDetails.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/AbstractFilesystemTableBackendDetails.java
index ca5eae58..e98e4fad 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/AbstractFilesystemTableBackendDetails.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/AbstractFilesystemTableBackendDetails.java
@@ -30,42 +30,78 @@ import com.kingsrook.qqq.backend.core.model.metadata.QTableBackendDetails;
*******************************************************************************/
public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
{
- private String path;
- private String recordFormat; // todo - enum? but hard w/ serialization?
- private String cardinality; // todo - enum?
+ private String basePath;
+ private String glob;
+ private RecordFormat recordFormat;
+ private Cardinality cardinality;
/*******************************************************************************
- ** Getter for path
+ ** Getter for basePath
**
*******************************************************************************/
- public String getPath()
+ public String getBasePath()
{
- return path;
+ return basePath;
}
/*******************************************************************************
- ** Setter for path
+ ** Setter for basePath
**
*******************************************************************************/
- public void setPath(String path)
+ public void setBasePath(String basePath)
{
- this.path = path;
+ this.basePath = basePath;
}
/*******************************************************************************
- ** Fluent Setter for path
+ ** Fluent Setter for basePath
**
*******************************************************************************/
@SuppressWarnings("unchecked")
- public T withPath(String path)
+ public T withBasePath(String basePath)
{
- this.path = path;
+ this.basePath = basePath;
+ return (T) this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for glob
+ **
+ *******************************************************************************/
+ public String getGlob()
+ {
+ return glob;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for glob
+ **
+ *******************************************************************************/
+ public void setGlob(String glob)
+ {
+ this.glob = glob;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent Setter for glob
+ **
+ *******************************************************************************/
+ @SuppressWarnings("unchecked")
+ public T withGlob(String glob)
+ {
+ this.glob = glob;
return (T) this;
}
@@ -75,7 +111,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
** Getter for recordFormat
**
*******************************************************************************/
- public String getRecordFormat()
+ public RecordFormat getRecordFormat()
{
return recordFormat;
}
@@ -86,7 +122,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
** Setter for recordFormat
**
*******************************************************************************/
- public void setRecordFormat(String recordFormat)
+ public void setRecordFormat(RecordFormat recordFormat)
{
this.recordFormat = recordFormat;
}
@@ -98,7 +134,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
**
*******************************************************************************/
@SuppressWarnings("unchecked")
- public T withRecordFormat(String recordFormat)
+ public T withRecordFormat(RecordFormat recordFormat)
{
this.recordFormat = recordFormat;
return ((T) this);
@@ -110,7 +146,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
** Getter for cardinality
**
*******************************************************************************/
- public String getCardinality()
+ public Cardinality getCardinality()
{
return cardinality;
}
@@ -121,7 +157,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
** Setter for cardinality
**
*******************************************************************************/
- public void setCardinality(String cardinality)
+ public void setCardinality(Cardinality cardinality)
{
this.cardinality = cardinality;
}
@@ -133,7 +169,7 @@ public class AbstractFilesystemTableBackendDetails extends QTableBackendDetails
**
*******************************************************************************/
@SuppressWarnings("unchecked")
- public T withCardinality(String cardinality)
+ public T withCardinality(Cardinality cardinality)
{
this.cardinality = cardinality;
return ((T) this);
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/Cardinality.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/Cardinality.java
new file mode 100644
index 00000000..0fae2ae6
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/Cardinality.java
@@ -0,0 +1,33 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.filesystem.base.model.metadata;
+
+
+/*******************************************************************************
+ ** Mode for filesystem backends: are all records in a single file, or are there
+ ** many files?
+ *******************************************************************************/
+public enum Cardinality
+{
+ ONE,
+ MANY
+}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/RecordFormat.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/RecordFormat.java
new file mode 100644
index 00000000..572876de
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/base/model/metadata/RecordFormat.java
@@ -0,0 +1,34 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.filesystem.base.model.metadata;
+
+
+/*******************************************************************************
+ ** How are records stored in the files in a filesystem backend? CSV, JSON,
+ ** future types may XML, or more exotic ones, like "BINARY" or "TEXT" (e.g., 1
+ ** record and 1 field per-file)
+ *******************************************************************************/
+public enum RecordFormat
+{
+ CSV,
+ JSON
+}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java
index 09594b99..f432ec7b 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/FilesystemBackendModule.java
@@ -24,16 +24,15 @@ package com.kingsrook.qqq.backend.module.filesystem.local;
import java.io.File;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableBackendDetails;
-import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.modules.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.modules.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.modules.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
-import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
+import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
+import com.kingsrook.qqq.backend.module.filesystem.local.actions.AbstractFilesystemAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemDeleteAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemInsertAction;
import com.kingsrook.qqq.backend.module.filesystem.local.actions.FilesystemQueryAction;
@@ -52,6 +51,19 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys
private static final Logger LOG = LogManager.getLogger(FilesystemBackendModule.class);
+
+ /*******************************************************************************
+ ** For filesystem backends, get the module-specific action base-class, that helps
+ ** with functions like listing and deleting files.
+ *******************************************************************************/
+ @Override
+ public AbstractBaseFilesystemAction getActionBase()
+ {
+ return (new AbstractFilesystemAction());
+ }
+
+
+
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
@@ -127,70 +139,4 @@ public class FilesystemBackendModule implements QBackendModuleInterface, Filesys
return (new FilesystemDeleteAction());
}
-
-
- /*******************************************************************************
- ** In contrast with the DeleteAction, which deletes RECORDS - this is a
- ** filesystem-(or s3, sftp, etc)-specific extension to delete an entire FILE
- ** e.g., for post-ETL.
- **
- ** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
- *******************************************************************************/
- @Override
- public void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException
- {
- File file = new File(fileReference);
- if(!file.exists())
- {
- //////////////////////////////////////////////////////////////////////////////////////////////
- // if the file doesn't exist, just exit with noop. don't throw an error - that should only //
- // happen if the "contract" of the method is broken, and the file still exists //
- //////////////////////////////////////////////////////////////////////////////////////////////
- LOG.debug("Not deleting file [{}], because it does not exist.", file);
- return;
- }
-
- if(!file.delete())
- {
- throw (new FilesystemException("Failed to delete file: " + fileReference));
- }
- }
-
-
-
- /*******************************************************************************
- ** Move a file from a source path, to a destination path.
- **
- ** @throws FilesystemException if the delete is known to have failed
- *******************************************************************************/
- @Override
- public void moveFile(QInstance instance, QTableMetaData table, String source, String destination) throws FilesystemException
- {
- File sourceFile = new File(source);
- File destinationFile = new File(destination);
- File destinationParent = destinationFile.getParentFile();
-
- if(!sourceFile.exists())
- {
- throw (new FilesystemException("Cannot move file " + source + ", as it does not exist."));
- }
-
- //////////////////////////////////////////////////////////////////////////////////////
- // if the destination folder doesn't exist, try to make it - and fail if that fails //
- //////////////////////////////////////////////////////////////////////////////////////
- if(!destinationParent.exists())
- {
- LOG.debug("Making destination directory {} for move", destinationParent.getAbsolutePath());
- if(!destinationParent.mkdirs())
- {
- throw (new FilesystemException("Failed to make destination directory " + destinationParent.getAbsolutePath() + " to move " + source + " into."));
- }
- }
-
- if(!sourceFile.renameTo(destinationFile))
- {
- throw (new FilesystemException("Failed to move (rename) file " + source + " to " + destination));
- }
- }
-
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java
index 91b5654b..74ddadc2 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/AbstractFilesystemAction.java
@@ -29,11 +29,14 @@ import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
-import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
+import com.kingsrook.qqq.backend.module.filesystem.exceptions.FilesystemException;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
@@ -41,6 +44,9 @@ import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFile
*******************************************************************************/
public class AbstractFilesystemAction extends AbstractBaseFilesystemAction
{
+ private static final Logger LOG = LogManager.getLogger(AbstractFilesystemAction.class);
+
+
/*******************************************************************************
** List the files for this table.
@@ -48,7 +54,8 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction
@Override
public List listFiles(QTableMetaData table, QBackendMetaData backendBase)
{
- String fullPath = getFullPath(table, backendBase);
+ // todo - needs rewritten to do globbing...
+ String fullPath = getFullBasePath(table, backendBase);
File directory = new File(fullPath);
File[] files = directory.listFiles();
@@ -74,15 +81,106 @@ public class AbstractFilesystemAction extends AbstractBaseFilesystemAction
/*******************************************************************************
- ** Add backend details to records about the file that they are in.
+ ** Write a file - to be implemented in module-specific subclasses.
*******************************************************************************/
@Override
- protected void addBackendDetailsToRecords(List recordsInFile, File file)
+ public void writeFile(QBackendMetaData backend, String path, byte[] contents) throws IOException
{
- recordsInFile.forEach(record ->
+ FileUtils.writeByteArrayToFile(new File(path), contents);
+ }
+
+
+
+ /*******************************************************************************
+ ** Get a string that represents the full path to a file.
+ *******************************************************************************/
+ @Override
+ protected String getFullPathForFile(File file)
+ {
+ return (file.getAbsolutePath());
+ }
+
+
+
+ /*******************************************************************************
+ ** In contrast with the DeleteAction, which deletes RECORDS - this is a
+ ** filesystem-(or s3, sftp, etc)-specific extension to delete an entire FILE
+ ** e.g., for post-ETL.
+ **
+ ** @throws FilesystemException if the delete is known to have failed, and the file is thought to still exit
+ *******************************************************************************/
+ @Override
+ public void deleteFile(QInstance instance, QTableMetaData table, String fileReference) throws FilesystemException
+ {
+ File file = new File(fileReference);
+ if(!file.exists())
{
- record.withBackendDetail(FilesystemRecordBackendDetailFields.FULL_PATH, file.getAbsolutePath());
- });
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // if the file doesn't exist, just exit with noop. don't throw an error - that should only //
+ // happen if the "contract" of the method is broken, and the file still exists //
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ LOG.debug("Not deleting file [{}], because it does not exist.", file);
+ return;
+ }
+
+ if(!file.delete())
+ {
+ throw (new FilesystemException("Failed to delete file: " + fileReference));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Move a file from a source path, to a destination path.
+ **
+ ** @param destination assumed to be a file path - not a directory
+ ** @throws FilesystemException if the delete is known to have failed
+ *******************************************************************************/
+ @Override
+ public void moveFile(QInstance instance, QTableMetaData table, String source, String destination) throws FilesystemException
+ {
+ File sourceFile = new File(source);
+ File destinationFile = new File(destination);
+ File destinationParent = destinationFile.getParentFile();
+
+ if(!sourceFile.exists())
+ {
+ throw (new FilesystemException("Cannot move file " + source + ", as it does not exist."));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // if the destination folder doesn't exist, try to make it - and fail if that fails //
+ //////////////////////////////////////////////////////////////////////////////////////
+ if(!destinationParent.exists())
+ {
+ LOG.debug("Making destination directory {} for move", destinationParent.getAbsolutePath());
+ if(!destinationParent.mkdirs())
+ {
+ throw (new FilesystemException("Failed to make destination directory " + destinationParent.getAbsolutePath() + " to move " + source + " into."));
+ }
+ }
+
+ if(!sourceFile.renameTo(destinationFile))
+ {
+ throw (new FilesystemException("Failed to move (rename) file " + source + " to " + destination));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** e.g., with a base path of /foo/
+ ** and a table path of /bar/
+ ** and a file at /foo/bar/baz.txt
+ ** give us just the baz.txt part.
+ *******************************************************************************/
+ @Override
+ public String stripBackendAndTableBasePathsFromFileName(File file, QBackendMetaData backend, QTableMetaData table)
+ {
+ String tablePath = getFullBasePath(table, backend);
+ String strippedPath = file.getAbsolutePath().replaceFirst(".*" + tablePath, "");
+ return (strippedPath);
}
}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCleanupSourceFilesFunction.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCleanupSourceFilesFunction.java
index e35ad59b..4e096635 100644
--- a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCleanupSourceFilesFunction.java
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCleanupSourceFilesFunction.java
@@ -23,8 +23,6 @@ package com.kingsrook.qqq.backend.module.filesystem.processes.implementations.et
import java.io.File;
-import java.util.Set;
-import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.interfaces.FunctionBody;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest;
@@ -43,7 +41,8 @@ import com.kingsrook.qqq.backend.core.modules.interfaces.QBackendModuleInterface
import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
-import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
/*******************************************************************************
@@ -52,11 +51,20 @@ import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendD
*******************************************************************************/
public class BasicETLCleanupSourceFilesFunction implements FunctionBody
{
+ private static final Logger LOG = LogManager.getLogger(BasicETLCleanupSourceFilesFunction.class);
+
public static final String FIELD_MOVE_OR_DELETE = "moveOrDelete";
public static final String FIELD_DESTINATION_FOR_MOVES = "destinationForMoves";
+ public static final String VALUE_MOVE = "move";
+ public static final String VALUE_DELETE = "delete";
+ public static final String FUNCTION_NAME = "cleanupSourceFiles";
+
+ /*******************************************************************************
+ ** Execute the function - using the request as input, and the result as output.
+ *******************************************************************************/
@Override
public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) throws QException
{
@@ -70,17 +78,22 @@ public class BasicETLCleanupSourceFilesFunction implements FunctionBody
throw (new QException("Backend " + table.getBackendName() + " for table " + sourceTableName + " does not support this function."));
}
- Set sourceFiles = runFunctionRequest.getRecords().stream()
- .map(record -> record.getBackendDetailString(FilesystemRecordBackendDetailFields.FULL_PATH))
- .collect(Collectors.toSet());
+ String sourceFilePaths = runFunctionRequest.getValueString(BasicETLCollectSourceFileNamesFunction.FIELD_SOURCE_FILE_PATHS);
+ if(!StringUtils.hasContent(sourceFilePaths))
+ {
+ LOG.info("No source file paths were specified in field [" + BasicETLCollectSourceFileNamesFunction.FIELD_SOURCE_FILE_PATHS + "]");
+ return;
+ }
+
+ String[] sourceFiles = sourceFilePaths.split(",");
for(String sourceFile : sourceFiles)
{
String moveOrDelete = runFunctionRequest.getValueString(FIELD_MOVE_OR_DELETE);
- if("delete".equals(moveOrDelete))
+ if(VALUE_DELETE.equals(moveOrDelete))
{
- filesystemModule.deleteFile(runFunctionRequest.getInstance(), table, sourceFile);
+ filesystemModule.getActionBase().deleteFile(runFunctionRequest.getInstance(), table, sourceFile);
}
- else if("move".equals(moveOrDelete))
+ else if(VALUE_MOVE.equals(moveOrDelete))
{
String destinationForMoves = runFunctionRequest.getValueString(FIELD_DESTINATION_FOR_MOVES);
if(!StringUtils.hasContent(destinationForMoves))
@@ -89,11 +102,12 @@ public class BasicETLCleanupSourceFilesFunction implements FunctionBody
}
File file = new File(sourceFile);
String destinationPath = destinationForMoves + File.separator + file.getName();
- filesystemModule.moveFile(runFunctionRequest.getInstance(), table, sourceFile, destinationPath);
+ filesystemModule.getActionBase().moveFile(runFunctionRequest.getInstance(), table, sourceFile, destinationPath);
}
else
{
- throw (new QException("Unexpected value [" + moveOrDelete + "] for field [" + FIELD_MOVE_OR_DELETE + "]. Must be either [move] or [delete]."));
+ throw (new QException("Unexpected value [" + moveOrDelete + "] for field [" + FIELD_MOVE_OR_DELETE + "]. "
+ + "Must be either [" + VALUE_MOVE + "] or [" + VALUE_DELETE + "]."));
}
}
}
@@ -101,12 +115,12 @@ public class BasicETLCleanupSourceFilesFunction implements FunctionBody
/*******************************************************************************
- **
+ ** define the metaData that describes this function
*******************************************************************************/
public QFunctionMetaData defineFunctionMetaData()
{
return (new QFunctionMetaData()
- .withName("cleanupSourceFiles")
+ .withName(FUNCTION_NAME)
.withCode(new QCodeReference()
.withName(this.getClass().getName())
.withCodeType(QCodeType.JAVA)
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCollectSourceFileNamesFunction.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCollectSourceFileNamesFunction.java
new file mode 100644
index 00000000..20de530f
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/etl/basic/BasicETLCollectSourceFileNamesFunction.java
@@ -0,0 +1,84 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.filesystem.processes.implementations.etl.basic;
+
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.interfaces.FunctionBody;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult;
+import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.QCodeUsage;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemRecordBackendDetailFields;
+
+
+/*******************************************************************************
+ ** Function body for collecting the file names that were discovered in the
+ ** Extract step. These will be lost during the transform, so we capture them here,
+ ** so that our Clean function can move or delete them.
+ **
+ ** TODO - need unit test!!
+ *******************************************************************************/
+public class BasicETLCollectSourceFileNamesFunction implements FunctionBody
+{
+ public static final String FUNCTION_NAME = "collectSourceFileNames";
+ public static final String FIELD_SOURCE_FILE_PATHS = "sourceFilePaths";
+
+
+
+ /*******************************************************************************
+ ** Execute the function - using the request as input, and the result as output.
+ *******************************************************************************/
+ @Override
+ public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) throws QException
+ {
+ Set sourceFiles = runFunctionRequest.getRecords().stream()
+ .map(record -> record.getBackendDetailString(FilesystemRecordBackendDetailFields.FULL_PATH))
+ .collect(Collectors.toSet());
+ runFunctionResult.addValue(FIELD_SOURCE_FILE_PATHS, StringUtils.join(",", sourceFiles));
+ }
+
+
+
+ /*******************************************************************************
+ ** define the metaData that describes this function
+ *******************************************************************************/
+ public QFunctionMetaData defineFunctionMetaData()
+ {
+ return (new QFunctionMetaData()
+ .withName(FUNCTION_NAME)
+ .withCode(new QCodeReference()
+ .withName(this.getClass().getName())
+ .withCodeType(QCodeType.JAVA)
+ .withCodeUsage(QCodeUsage.FUNCTION))
+ .withOutputMetaData(new QFunctionOutputMetaData()
+ .addField(new QFieldMetaData(FIELD_SOURCE_FILE_PATHS, QFieldType.STRING))));
+ }
+}
diff --git a/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncFunction.java b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncFunction.java
new file mode 100644
index 00000000..be043bc7
--- /dev/null
+++ b/src/main/java/com/kingsrook/qqq/backend/module/filesystem/processes/implementations/filesystem/sync/FilesystemSyncFunction.java
@@ -0,0 +1,123 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.module.filesystem.processes.implementations.filesystem.sync;
+
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.interfaces.FunctionBody;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
+import com.kingsrook.qqq.backend.core.modules.QBackendModuleDispatcher;
+import com.kingsrook.qqq.backend.module.filesystem.base.FilesystemBackendModuleInterface;
+import com.kingsrook.qqq.backend.module.filesystem.base.actions.AbstractBaseFilesystemAction;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ ** Function body for collecting the file names that were discovered in the
+ ** Extract step. These will be lost during the transform, so we capture them here,
+ ** so that our Clean function can move or delete them.
+ **
+ *******************************************************************************/
+public class FilesystemSyncFunction implements FunctionBody
+{
+ private static final Logger LOG = LogManager.getLogger(FilesystemSyncFunction.class);
+
+ public static final String FUNCTION_NAME = "sync";
+
+
+
+ /*******************************************************************************
+ ** Execute the function - using the request as input, and the result as output.
+ *******************************************************************************/
+ @Override
+ public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) throws QException
+ {
+ QTableMetaData sourceTable = runFunctionRequest.getInstance().getTable(runFunctionRequest.getValueString(FilesystemSyncProcess.FIELD_SOURCE_TABLE));
+ QTableMetaData archiveTable = runFunctionRequest.getInstance().getTable(runFunctionRequest.getValueString(FilesystemSyncProcess.FIELD_ARCHIVE_TABLE));
+ QTableMetaData processingTable = runFunctionRequest.getInstance().getTable(runFunctionRequest.getValueString(FilesystemSyncProcess.FIELD_PROCESSING_TABLE));
+
+ QBackendMetaData sourceBackend = runFunctionRequest.getInstance().getBackendForTable(sourceTable.getName());
+ FilesystemBackendModuleInterface sourceModule = (FilesystemBackendModuleInterface) new QBackendModuleDispatcher().getQBackendModule(sourceBackend);
+ Map sourceFiles = getFileNames(sourceModule.getActionBase(), sourceTable, sourceBackend);
+
+ QBackendMetaData archiveBackend = runFunctionRequest.getInstance().getBackendForTable(archiveTable.getName());
+ FilesystemBackendModuleInterface archiveModule = (FilesystemBackendModuleInterface) new QBackendModuleDispatcher().getQBackendModule(archiveBackend);
+ Set archiveFiles = getFileNames(archiveModule.getActionBase(), archiveTable, archiveBackend).keySet();
+
+ QBackendMetaData processingBackend = runFunctionRequest.getInstance().getBackendForTable(processingTable.getName());
+ FilesystemBackendModuleInterface processingModule = (FilesystemBackendModuleInterface) new QBackendModuleDispatcher().getQBackendModule(processingBackend);
+
+ for(Map.Entry sourceEntry : sourceFiles.entrySet())
+ {
+ try
+ {
+ String sourceFileName = sourceEntry.getKey();
+ if(!archiveFiles.contains(sourceFileName))
+ {
+ LOG.info("Syncing file [" + sourceFileName + "] to [" + archiveTable + "] and [" + processingTable + "]");
+ InputStream inputStream = sourceModule.getActionBase().readFile(sourceEntry.getValue());
+ byte[] bytes = inputStream.readAllBytes();
+
+ String archivePath = archiveModule.getActionBase().getFullBasePath(archiveTable, archiveBackend);
+ archiveModule.getActionBase().writeFile(archiveBackend, archivePath + File.separator + sourceFileName, bytes);
+
+ String processingPath = processingModule.getActionBase().getFullBasePath(processingTable, processingBackend);
+ processingModule.getActionBase().writeFile(processingBackend, processingPath + File.separator + sourceFileName, bytes);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error processing file: " + sourceEntry, e);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Map getFileNames(AbstractBaseFilesystemAction actionBase, QTableMetaData table, QBackendMetaData backend)
+ {
+ List