diff --git a/lib/build/SparseBitSet-1.3.jar b/lib/build/SparseBitSet-1.3.jar
new file mode 100644
index 00000000..5a435441
Binary files /dev/null and b/lib/build/SparseBitSet-1.3.jar differ
diff --git a/lib/build/commons-math3-3.6.1.jar b/lib/build/commons-math3-3.6.1.jar
new file mode 100644
index 00000000..0ff582cf
Binary files /dev/null and b/lib/build/commons-math3-3.6.1.jar differ
diff --git a/lib/build/curvesapi-1.08.jar b/lib/build/curvesapi-1.08.jar
new file mode 100644
index 00000000..18fb71b1
Binary files /dev/null and b/lib/build/curvesapi-1.08.jar differ
diff --git a/lib/build/log4j-api-2.24.3.jar b/lib/build/log4j-api-2.24.3.jar
new file mode 100644
index 00000000..4af1681f
Binary files /dev/null and b/lib/build/log4j-api-2.24.3.jar differ
diff --git a/lib/build/poi-5.5.1.jar b/lib/build/poi-5.5.1.jar
new file mode 100644
index 00000000..702effe8
Binary files /dev/null and b/lib/build/poi-5.5.1.jar differ
diff --git a/lib/build/poi-ooxml-5.5.1.jar b/lib/build/poi-ooxml-5.5.1.jar
new file mode 100644
index 00000000..dcf1044b
Binary files /dev/null and b/lib/build/poi-ooxml-5.5.1.jar differ
diff --git a/lib/build/poi-ooxml-lite-5.5.1.jar b/lib/build/poi-ooxml-lite-5.5.1.jar
new file mode 100644
index 00000000..a9d53b76
Binary files /dev/null and b/lib/build/poi-ooxml-lite-5.5.1.jar differ
diff --git a/lib/build/xmlbeans-5.3.0.jar b/lib/build/xmlbeans-5.3.0.jar
new file mode 100644
index 00000000..2ba57e55
Binary files /dev/null and b/lib/build/xmlbeans-5.3.0.jar differ
diff --git a/pom.xml b/pom.xml
index 131be167..eda198fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,6 +113,11 @@
pdfbox
3.0.8
+
+ org.apache.poi
+ poi-ooxml
+ 5.5.1
+
commons-validator
commons-validator
diff --git a/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java b/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java
index 2f3151b8..ce78981d 100644
--- a/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java
+++ b/src/main/java/com/simisinc/platform/application/cms/PublishContentCommand.java
@@ -26,10 +26,10 @@
import com.simisinc.platform.domain.model.cms.WebPage;
import com.simisinc.platform.infrastructure.persistence.cms.ContentRepository;
import com.simisinc.platform.infrastructure.persistence.cms.WebPageRepository;
-import com.simisinc.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob;
import com.simisinc.platform.infrastructure.workflow.WorkflowManager;
import com.zeroio.platform.domain.model.cms.ContentVersion;
import com.zeroio.platform.infrastructure.persistence.cms.ContentVersionRepository;
+import com.zeroio.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob;
/**
* Publishes draft content to live
diff --git a/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java b/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java
index c6156d46..9bf06832 100644
--- a/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java
+++ b/src/main/java/com/simisinc/platform/application/cms/SaveContentCommand.java
@@ -24,9 +24,9 @@
import com.simisinc.platform.application.DataException;
import com.simisinc.platform.domain.model.cms.Content;
import com.simisinc.platform.infrastructure.persistence.cms.ContentRepository;
-import com.simisinc.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob;
import com.zeroio.platform.domain.model.cms.ContentVersion;
import com.zeroio.platform.infrastructure.persistence.cms.ContentVersionRepository;
+import com.zeroio.platform.infrastructure.scheduler.cms.RefreshWebPageTextIndexJob;
/**
* Validates and saves content objects
diff --git a/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java b/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java
index 7b65b14a..7e9e6ff6 100644
--- a/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java
+++ b/src/main/java/com/simisinc/platform/domain/model/cms/FileItem.java
@@ -1,4 +1,5 @@
/*
+ * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski)
* Copyright 2022 SimIS Inc. (https://www.simiscms.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,6 +45,7 @@ public class FileItem extends Entity {
private String fileType = null;
private String mimeType = null;
private String fileHash = null;
+ private String documentText = null;
private String webPath = null;
private int width = -1;
private int height = -1;
@@ -175,6 +177,14 @@ public void setFileHash(String fileHash) {
this.fileHash = fileHash;
}
+ public String getDocumentText() {
+ return documentText;
+ }
+
+ public void setDocumentText(String documentText) {
+ this.documentText = documentText;
+ }
+
public int getWidth() {
return width;
}
diff --git a/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java b/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java
index c0c77ffd..ce154e08 100644
--- a/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java
+++ b/src/main/java/com/simisinc/platform/domain/model/items/ItemFileItem.java
@@ -1,4 +1,5 @@
/*
+ * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski)
* Copyright 2022 SimIS Inc. (https://www.simiscms.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,6 +45,7 @@ public class ItemFileItem extends Entity {
private String fileType = null;
private String mimeType = null;
private String fileHash = null;
+ private String documentText = null;
private String webPath = null;
private int width = -1;
private int height = -1;
@@ -183,6 +185,14 @@ public void setFileHash(String fileHash) {
this.fileHash = fileHash;
}
+ public String getDocumentText() {
+ return documentText;
+ }
+
+ public void setDocumentText(String documentText) {
+ this.documentText = documentText;
+ }
+
public int getWidth() {
return width;
}
@@ -302,7 +312,8 @@ public String getWebPath() {
public void setWebPath(String webPath) {
this.webPath = webPath;
}
-public String getUrl() {
+
+ public String getUrl() {
return webPath + "-" + id + "/" + UrlCommand.encodeUri(filename);
- }
+ }
}
diff --git a/src/main/java/com/simisinc/platform/infrastructure/database/DB.java b/src/main/java/com/simisinc/platform/infrastructure/database/DB.java
index a34288c3..6aca88be 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/database/DB.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/database/DB.java
@@ -419,6 +419,29 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlWhe
public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoins joins, SqlWhere where,
SqlUtils orderBy, DataConstraints constraints, Function buildRecord) {
+ // This method creates a TABLE_NAME.* column automatically (but may be deprecated in the future)
+ if (select == null) {
+ select = DB.SELECT();
+ }
+ // At the front of the columns, add the table's full columns for backwards compatibility
+ select.getValues().add(0, new SqlValue(tableName + ".*"));
+ return selectFrom(tableName, select, joins, where, orderBy, constraints, buildRecord);
+ }
+
+ /**
+ * Select records from the database with the specified parameters.
+ *
+ * @param tableName the name of the table
+ * @param columns the columns values
+ * @param joins the join clauses
+ * @param where the where clause
+ * @param orderBy the order by clause
+ * @param constraints the data constraints
+ * @param buildRecord the function to build an entity from the result set
+ * @return the data result containing the records
+ */
+ public static DataResult selectFrom(String tableName, SqlUtils columns, SqlJoins joins, SqlWhere where,
+ SqlUtils orderBy, DataConstraints constraints, Function buildRecord) {
// Determine the max records based on the where conditions
DataResult dataResult = new DataResult();
@@ -462,10 +485,8 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoi
// Prepare the query
StringBuilder sb = new StringBuilder();
- sb.append("SELECT ").append(tableName).append(".*");
- if (select != null) {
- sb.append(createAdditionalSelectFields(select));
- }
+ sb.append("SELECT ");
+ sb.append(createSelectFields(columns));
sb.append(" FROM ").append(tableName);
sb.append(joinsSb);
sb.append(whereSb);
@@ -489,7 +510,7 @@ public static DataResult selectAllFrom(String tableName, SqlUtils select, SqlJoi
List records = null;
long startQueryTime = System.currentTimeMillis();
try (Connection connection = getConnection();
- PreparedStatement pst = createPreparedStatement(connection, sb.toString(), select, where, orderBy);
+ PreparedStatement pst = createPreparedStatement(connection, sb.toString(), columns, where, orderBy);
ResultSet rs = pst.executeQuery()) {
records = new ArrayList<>();
while (rs.next()) {
diff --git a/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java b/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java
index 42394e07..f6fd4a65 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/database/SqlWhere.java
@@ -35,6 +35,8 @@ public class SqlWhere {
public static final String AND_OPERATOR = "AND";
public static final String OR_OPERATOR = "OR";
+ public static final String NOT_AND_OPERATOR = "NOT AND";
+ public static final String NOT_OR_OPERATOR = "OR";
private List values = new ArrayList<>();
@@ -296,10 +298,20 @@ private static String arrayToSqlStatementList(String jsonbColumnName, String[] a
if (StringUtils.isBlank(jsonbColumnName) || arrayValues == null || arrayValues.length == 0) {
return null;
}
+ StringBuilder sql = new StringBuilder();
// Default to AND if operator is null or invalid
- String op = OR_OPERATOR.equalsIgnoreCase(operator) ? OR_OPERATOR : AND_OPERATOR;
- StringBuilder sql = new StringBuilder("(");
+ String op = (OR_OPERATOR.equalsIgnoreCase(operator) || NOT_OR_OPERATOR.equalsIgnoreCase(operator))
+ ? OR_OPERATOR
+ : AND_OPERATOR;
+
+ // If the operator is a NOT variant, prepend "NOT " to the condition
+ if (NOT_AND_OPERATOR.equalsIgnoreCase(operator) || NOT_OR_OPERATOR.equalsIgnoreCase(operator)) {
+ sql.append("NOT ");
+ }
+
+ // Build the condition with one placeholder per value
+ sql.append("(");
for (int i = 0; i < arrayValues.length; i++) {
if (i > 0) {
sql.append(" ").append(op).append(" ");
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java
index fe376e4e..e958f783 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileItemRepository.java
@@ -61,11 +61,51 @@ public class FileItemRepository {
private static String[] PRIMARY_KEY = new String[] { "file_id" };
private static DataResult query(FileSpecification specification, DataConstraints constraints) {
- SqlUtils select = new SqlUtils();
+
+ SqlUtils select = DB.SELECT(
+ TABLE_NAME + ".file_id",
+ TABLE_NAME + ".folder_id",
+ TABLE_NAME + ".filename",
+ TABLE_NAME + ".title",
+ TABLE_NAME + ".barcode",
+ TABLE_NAME + ".version",
+ TABLE_NAME + ".extension",
+ TABLE_NAME + ".path",
+ TABLE_NAME + ".file_length",
+ TABLE_NAME + ".file_type",
+ TABLE_NAME + ".mime_type",
+ TABLE_NAME + ".file_hash",
+ TABLE_NAME + ".width",
+ TABLE_NAME + ".height",
+ TABLE_NAME + ".summary",
+ TABLE_NAME + ".created_by",
+ TABLE_NAME + ".created",
+ TABLE_NAME + ".modified_by",
+ TABLE_NAME + ".modified",
+ TABLE_NAME + ".processed",
+ TABLE_NAME + ".expiration_date",
+ TABLE_NAME + ".privacy_type",
+ TABLE_NAME + ".default_token",
+ TABLE_NAME + ".version_count",
+ TABLE_NAME + ".download_count",
+ TABLE_NAME + ".sub_folder_id",
+ TABLE_NAME + ".category_id",
+ TABLE_NAME + ".web_path",
+ TABLE_NAME + ".tags");
+
SqlJoins joins = new SqlJoins();
SqlWhere where = DB.WHERE();
SqlUtils orderBy = new SqlUtils();
+
+ final boolean includeDocumentText = specification != null && specification.getIncludeDocumentText();
+
if (specification != null) {
+
+ // Only include this potentially-large field when requested
+ if (includeDocumentText) {
+ select.add(TABLE_NAME + ".document_text");
+ }
+
joins.add("LEFT JOIN folders ON (files.folder_id = folders.folder_id)");
where
.andAddIfHasValue("file_id = ?", specification.getId(), -1)
@@ -79,6 +119,10 @@ private static DataResult query(FileSpecification specification, DataConstraints
where.AND("LOWER(files.file_type) = ANY(?)",
Arrays.stream(specification.getFileType()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY);
}
+ if (specification.getFileExtension() != null) {
+ where.AND("LOWER(files.extension) = ANY(?)",
+ Arrays.stream(specification.getFileExtension()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY);
+ }
if (specification.getMatchesName() != null) {
String likeValue = specification.getMatchesName().trim()
.replace("!", "!!")
@@ -97,6 +141,13 @@ private static DataResult query(FileSpecification specification, DataConstraints
where.AND("sub_folder_id IS NULL");
}
}
+ if (specification.getIsProcessed() != DataConstants.UNDEFINED) {
+ if (specification.getIsProcessed() == DataConstants.TRUE) {
+ where.AND("processed IS NOT NULL");
+ } else {
+ where.AND("processed IS NULL");
+ }
+ }
// For user id
// User must be in a user group with folder access
@@ -143,6 +194,10 @@ private static DataResult query(FileSpecification specification, DataConstraints
where.AND("tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR);
}
+ if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) {
+ where.AND("tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR);
+ }
+
// Add modified date range filters
// Some imported files can have a null modified date, so fall back to created.
if (specification.getModifiedAfter() != null) {
@@ -167,8 +222,10 @@ private static DataResult query(FileSpecification specification, DataConstraints
where.AND(userCondition.toString(), (Object[]) userIdsBoxed);
}
}
- return DB.selectAllFrom(
- TABLE_NAME, select, joins, where, orderBy, constraints, FileItemRepository::buildRecord);
+
+ return DB.selectFrom(
+ TABLE_NAME, select, joins, where, orderBy, constraints,
+ rs -> buildRecord(rs, includeDocumentText));
}
public static FileItem findById(long id) {
@@ -369,6 +426,21 @@ public static FileItem saveVersion(FileItem record) {
return null;
}
+ public static boolean updateDocumentText(FileItem record, String documentText) {
+ try (Connection connection = DB.getConnection()) {
+ SqlUtils updateValues = new SqlUtils()
+ .add("document_text", documentText)
+ .add("processed", new Timestamp(System.currentTimeMillis()));
+ if (DB.update(connection, TABLE_NAME, updateValues, DB.WHERE("file_id = ?", record.getId()))) {
+ return true;
+ }
+ } catch (SQLException se) {
+ LOG.error("SQLException: " + se.getMessage());
+ }
+ LOG.error("The document text update failed!");
+ return false;
+ }
+
public static boolean remove(FileItem record) {
try (Connection connection = DB.getConnection();
AutoStartTransaction a = new AutoStartTransaction(connection);
@@ -427,6 +499,10 @@ public static long findTotalFileSize() {
}
private static FileItem buildRecord(ResultSet rs) {
+ return buildRecord(rs, false);
+ }
+
+ private static FileItem buildRecord(ResultSet rs, boolean includeDocumentText) {
try {
FileItem record = new FileItem();
record.setId(rs.getLong("file_id"));
@@ -463,6 +539,9 @@ private static FileItem buildRecord(ResultSet rs) {
record.setCategoryId(DB.getLong(rs, "category_id", -1L));
record.setWebPath(rs.getString("web_path"));
record.setTags(JsonCommand.fromJsonArray(rs.getString("tags")));
+ if (includeDocumentText) {
+ record.setDocumentText(rs.getString("document_text"));
+ }
return record;
} catch (SQLException se) {
LOG.error("buildRecord", se);
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java
index 032c1af9..0391525f 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/FileSpecification.java
@@ -37,15 +37,19 @@ public class FileSpecification extends Entity {
private String barcode = null;
private long createdBy = -1;
private String[] fileType = null;
+ private String[] fileExtension = null;
private Long forUserId = -1L;
private String matchesName = null;
private String searchName = null;
private String searchContent = null;
+ private boolean includeDocumentText = false;
private int withinLastDays = -1;
private int inASubFolder = DataConstants.UNDEFINED;
+ private int isProcessed = DataConstants.UNDEFINED;
private String versionWebPath = null;
private String[] regionTags = null;
private String[] filterTags = null;
+ private String[] excludeTags = null;
private Timestamp modifiedAfter = null;
private Timestamp modifiedBefore = null;
private long[] modifiedByUserIds = null;
@@ -129,6 +133,14 @@ public void setFileType(String value) {
}
}
+ public String[] getFileExtension() {
+ return fileExtension;
+ }
+
+ public void setFileExtension(String[] fileExtension) {
+ this.fileExtension = fileExtension;
+ }
+
public Long getForUserId() {
return forUserId;
}
@@ -161,6 +173,14 @@ public void setSearchContent(String searchContent) {
this.searchContent = searchContent;
}
+ public boolean getIncludeDocumentText() {
+ return includeDocumentText;
+ }
+
+ public void setIncludeDocumentText(boolean includeDocumentText) {
+ this.includeDocumentText = includeDocumentText;
+ }
+
public int getWithinLastDays() {
return withinLastDays;
}
@@ -181,6 +201,18 @@ public void setInASubFolder(boolean inASubFolder) {
this.inASubFolder = (inASubFolder ? DataConstants.TRUE : DataConstants.FALSE);
}
+ public int getIsProcessed() {
+ return isProcessed;
+ }
+
+ public void setIsProcessed(int isProcessed) {
+ this.isProcessed = isProcessed;
+ }
+
+ public void setIsProcessed(boolean isProcessed) {
+ this.isProcessed = (isProcessed ? DataConstants.TRUE : DataConstants.FALSE);
+ }
+
public String[] getFilterTags() {
return filterTags;
}
@@ -189,6 +221,14 @@ public void setFilterTags(String[] filterTags) {
this.filterTags = filterTags;
}
+ public String[] getExcludeTags() {
+ return excludeTags;
+ }
+
+ public void setExcludeTags(String[] excludeTags) {
+ this.excludeTags = excludeTags;
+ }
+
public String[] getRegionTags() {
return regionTags;
}
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java
index aef07f5f..af0e2c91 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageHitRepository.java
@@ -87,7 +87,8 @@ public static boolean remove(WebPageHit record) {
return false;
}
- private static PreparedStatement createPreparedStatementForDelete(Connection connection, WebPageHit record) throws SQLException {
+ private static PreparedStatement createPreparedStatementForDelete(Connection connection, WebPageHit record)
+ throws SQLException {
String SQL_QUERY = "DELETE FROM web_page_hits " +
"WHERE hit_id = ?";
int i = 0;
@@ -177,8 +178,10 @@ public static List findDailySessions(int daysToLimit) {
}
public static List findMonthlySessions(int monthsLimit) {
- String SQL_QUERY = "SELECT DATE_TRUNC('month', month)::VARCHAR(10) AS date_column, SUM(unique_sessions) AS monthly_count " +
- "FROM (SELECT generate_series(NOW() - INTERVAL '" + monthsLimit + " months', NOW(), INTERVAL '1 month')::date) d(month) " +
+ String SQL_QUERY = "SELECT DATE_TRUNC('month', month)::VARCHAR(10) AS date_column, SUM(unique_sessions) AS monthly_count "
+ +
+ "FROM (SELECT generate_series(NOW() - INTERVAL '" + monthsLimit
+ + " months', NOW(), INTERVAL '1 month')::date) d(month) " +
"LEFT JOIN web_page_hit_snapshots ON DATE_TRUNC('month', snapshot_date) = DATE_TRUNC('month', month) " +
"GROUP BY d.month " +
"ORDER BY d.month";
@@ -230,7 +233,8 @@ public static List findTopPaths(int value, char intervalType, in
"FROM web_page_hits " +
"WHERE hit_date > NOW() - INTERVAL '" + value + " " +
(intervalType == 'y' ? "years"
- : (intervalType == 'm' ? "months" : (intervalType == 'w' ? "weeks" : (intervalType == 'h' ? "hours" : "days"))))
+ : (intervalType == 'm' ? "months"
+ : (intervalType == 'w' ? "weeks" : (intervalType == 'h' ? "hours" : "days"))))
+
"' " +
"AND page_path NOT LIKE '/admin%' " +
@@ -346,7 +350,8 @@ public static List findTopAssets(int days, int recordLimit, String a
sqlQuery.append(" page_path LIKE '%.jpg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif' OR ");
sqlQuery.append(" page_path LIKE '%.zip' OR page_path LIKE '%.exe' OR page_path LIKE '%.ppt%' OR ");
sqlQuery.append(" page_path LIKE '%.drawio' OR page_path LIKE '%.vsdx') ");
- sqlQuery.append("AND NOT EXISTS (SELECT 1 FROM sessions WHERE session_id = web_page_hits.session_id AND is_bot = TRUE) ");
+ sqlQuery.append(
+ "AND NOT EXISTS (SELECT 1 FROM sessions WHERE session_id = web_page_hits.session_id AND is_bot = TRUE) ");
// Add asset type filter if specified
if (assetType != null && !assetType.trim().isEmpty()) {
@@ -368,7 +373,8 @@ public static List findTopAssets(int days, int recordLimit, String a
sqlQuery.append("page_path LIKE '%.drawio' OR page_path LIKE '%.vsdx'");
break;
case "Image":
- sqlQuery.append("page_path LIKE '%.jpg' OR page_path LIKE '%.jpeg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif'");
+ sqlQuery.append(
+ "page_path LIKE '%.jpg' OR page_path LIKE '%.jpeg' OR page_path LIKE '%.png' OR page_path LIKE '%.gif'");
break;
case "Archive":
sqlQuery.append("page_path LIKE '%.zip'");
@@ -578,24 +584,24 @@ public static List findDailyWebHitsForPage(String pagePath, java
return records;
}
- public static List findAuthenticatedUserVisitsForPage(String pagePath, int days, int recordLimit) {
+ public static List findAuthenticatedUserVisitsForPage(String pagePath, int days) {
java.time.LocalDate endDate = java.time.LocalDate.now();
java.time.LocalDate startDate = endDate.minusDays(Math.max(0, days - 1));
- return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate, recordLimit);
+ return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate);
}
public static List findAuthenticatedUserVisitsForPage(String pagePath, java.time.LocalDate fromDate,
- java.time.LocalDate toDate, int recordLimit) {
+ java.time.LocalDate toDate) {
java.time.LocalDate startDate = fromDate != null ? fromDate : toDate;
java.time.LocalDate endDate = toDate != null ? toDate : fromDate;
if (startDate == null || endDate == null) {
return null;
}
- return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate, recordLimit);
+ return findAuthenticatedUserVisitsForPageRange(pagePath, startDate, endDate);
}
- private static List findAuthenticatedUserVisitsForPageRange(String pagePath, java.time.LocalDate startDate,
- java.time.LocalDate endDate, int recordLimit) {
+ private static List findAuthenticatedUserVisitsForPageRange(String pagePath,
+ java.time.LocalDate startDate, java.time.LocalDate endDate) {
String normalizedPagePath = normalizePagePath(pagePath);
if (normalizedPagePath == null) {
return null;
@@ -622,15 +628,15 @@ private static List findAuthenticatedUserVisitsForPageRange(String p
" AND page_path = ? " +
" AND is_logged_in = TRUE " +
" AND session_id IS NOT NULL " +
- " AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.session_id = web_page_hits.session_id AND s.is_bot = TRUE) " +
+ " AND NOT EXISTS (SELECT 1 FROM sessions s WHERE s.session_id = web_page_hits.session_id AND s.is_bot = TRUE) "
+ +
" GROUP BY page_path, session_id " +
" ) page_visits " +
" JOIN user_logins ul ON ul.session_id = page_visits.session_id " +
" JOIN users u ON u.user_id = ul.user_id " +
" GROUP BY page_visits.page_path, ul.user_id, u.first_name, u.last_name, u.email, u.username " +
") member_visits " +
- "ORDER BY last_hit DESC " +
- "LIMIT " + recordLimit;
+ "ORDER BY last_hit DESC";
List records = null;
try (Connection connection = DB.getConnection();
PreparedStatement pst = connection.prepareStatement(SQL_QUERY)) {
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java
index c9d227a5..d777d41e 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageRepository.java
@@ -90,6 +90,10 @@ private static DataResult query(WebPageSpecification specification, DataConstrai
where.AND("web_pages.tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR);
}
+ if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) {
+ where.AND("web_pages.tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR);
+ }
+
// Add modified date range filters
if (specification.getModifiedAfter() != null) {
where.AND("web_pages.modified >= ?", specification.getModifiedAfter());
@@ -331,6 +335,45 @@ public static void markAsModifiedAndFindable(WebPage record, long userId) {
}
}
+ public static boolean archivePage(WebPage record, long userId) {
+ if (record == null || record.getId() == -1) {
+ return false;
+ }
+ // Explicitly set enabled to false (archived)
+ SqlUtils updateValues = new SqlUtils()
+ .add("enabled", false)
+ .add("modified", new Timestamp(System.currentTimeMillis()))
+ .add("modified_by", userId);
+ boolean updated = DB.update(TABLE_NAME, updateValues, DB.WHERE("web_page_id = ?", record.getId()));
+ if (updated) {
+ record.setEnabled(false);
+ record.setModifiedBy(userId);
+ // Note: We do NOT remove the page from cache when archiving
+ // The page layout/content hasn't changed, only the enabled status
+ // This allows admins/content-managers to still view the archived page
+ }
+ return updated;
+ }
+
+ public static boolean unarchivePage(WebPage record, long userId) {
+ if (record == null || record.getId() == -1) {
+ return false;
+ }
+ // Explicitly set enabled to true (unarchived)
+ SqlUtils updateValues = new SqlUtils()
+ .add("enabled", true)
+ .add("modified", new Timestamp(System.currentTimeMillis()))
+ .add("modified_by", userId);
+ boolean updated = DB.update(TABLE_NAME, updateValues, DB.WHERE("web_page_id = ?", record.getId()));
+ if (updated) {
+ record.setEnabled(true);
+ record.setModifiedBy(userId);
+ // Note: We do NOT remove the page from cache when unarchiving
+ // The page layout/content hasn't changed, only the enabled status
+ }
+ return updated;
+ }
+
public static void removeDraft(WebPage record) {
if (record == null || record.getId() == -1) {
return;
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java
index ba9431be..9d6e2226 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/cms/WebPageSpecification.java
@@ -38,6 +38,7 @@ public class WebPageSpecification extends Entity {
private int hasRedirect = DataConstants.UNDEFINED;
private String[] regionTags = null;
private String[] filterTags = null;
+ private String[] excludeTags = null;
private Timestamp modifiedAfter = null;
private Timestamp modifiedBefore = null;
private long[] modifiedByUserIds = null;
@@ -122,6 +123,14 @@ public void setFilterTags(String[] filterTags) {
this.filterTags = filterTags;
}
+ public String[] getExcludeTags() {
+ return excludeTags;
+ }
+
+ public void setExcludeTags(String[] excludeTags) {
+ this.excludeTags = excludeTags;
+ }
+
public String[] getRegionTags() {
return regionTags;
}
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java
index a273cdea..53a2ae92 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileItemRepository.java
@@ -61,12 +61,52 @@ public class ItemFileItemRepository {
private static String[] PRIMARY_KEY = new String[] { "file_id" };
private static DataResult query(ItemFileSpecification specification, DataConstraints constraints) {
- SqlUtils select = new SqlUtils();
+
+ SqlUtils select = DB.SELECT(
+ TABLE_NAME + ".file_id",
+ TABLE_NAME + ".item_id",
+ TABLE_NAME + ".folder_id",
+ TABLE_NAME + ".filename",
+ TABLE_NAME + ".title",
+ TABLE_NAME + ".barcode",
+ TABLE_NAME + ".version",
+ TABLE_NAME + ".extension",
+ TABLE_NAME + ".path",
+ TABLE_NAME + ".file_length",
+ TABLE_NAME + ".file_type",
+ TABLE_NAME + ".mime_type",
+ TABLE_NAME + ".file_hash",
+ TABLE_NAME + ".width",
+ TABLE_NAME + ".height",
+ TABLE_NAME + ".summary",
+ TABLE_NAME + ".created_by",
+ TABLE_NAME + ".created",
+ TABLE_NAME + ".modified_by",
+ TABLE_NAME + ".modified",
+ TABLE_NAME + ".processed",
+ TABLE_NAME + ".expiration_date",
+ TABLE_NAME + ".privacy_type",
+ TABLE_NAME + ".default_token",
+ TABLE_NAME + ".version_count",
+ TABLE_NAME + ".download_count",
+ TABLE_NAME + ".sub_folder_id",
+ TABLE_NAME + ".category_id",
+ TABLE_NAME + ".web_path",
+ TABLE_NAME + ".tags");
+
SqlJoins joins = new SqlJoins();
SqlWhere where = DB.WHERE();
SqlUtils orderBy = new SqlUtils();
+
+ final boolean includeDocumentText = specification != null && specification.getIncludeDocumentText();
+
if (specification != null) {
+ // Only include this potentially-large field when requested
+ if (includeDocumentText) {
+ select.add("item_files.document_text");
+ }
+
joins.add("LEFT JOIN item_folders ON (item_files.folder_id = item_folders.folder_id)");
if (specification.getCollectionId() > -1) {
joins.add("LEFT JOIN items ON (item_files.item_id = items.item_id)");
@@ -86,6 +126,10 @@ private static DataResult query(ItemFileSpecification specification, DataConstra
where.AND("LOWER(item_files.file_type) = ANY(?)",
Arrays.stream(specification.getFileType()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY);
}
+ if (specification.getFileExtension() != null) {
+ where.AND("LOWER(item_files.extension) = ANY(?)",
+ Arrays.stream(specification.getFileExtension()).map(String::toLowerCase).toArray(String[]::new), Types.ARRAY);
+ }
if (specification.getMatchesName() != null) {
String likeValue = specification.getMatchesName().trim()
.replace("!", "!!")
@@ -104,6 +148,13 @@ private static DataResult query(ItemFileSpecification specification, DataConstra
where.AND("sub_folder_id IS NULL");
}
}
+ if (specification.getIsProcessed() != DataConstants.UNDEFINED) {
+ if (specification.getIsProcessed() == DataConstants.TRUE) {
+ where.AND("processed IS NOT NULL");
+ } else {
+ where.AND("processed IS NULL");
+ }
+ }
// For user id
// User must be in a user group with folder access
@@ -134,8 +185,11 @@ private static DataResult query(ItemFileSpecification specification, DataConstra
orderBy.add("rank DESC, file_id");
}
}
- return DB.selectAllFrom(
- TABLE_NAME, select, joins, where, orderBy, constraints, ItemFileItemRepository::buildRecord);
+
+ return DB.selectFrom(
+ TABLE_NAME, select, joins, where, orderBy, constraints,
+ rs -> buildRecord(rs, includeDocumentText));
+
}
public static ItemFileItem findById(long id) {
@@ -205,6 +259,12 @@ public static List findAll(ItemFileSpecification specification, Da
return (List) result.getRecords();
}
+ public static long fileCount(long itemId) {
+ SqlWhere where = DB.WHERE();
+ where.AND("item_id = ?", itemId);
+ return DB.selectCountFrom(TABLE_NAME, where);
+ }
+
public static ItemFileItem save(ItemFileItem record) {
if (record.getId() > -1) {
return update(record);
@@ -365,6 +425,21 @@ public static ItemFileItem saveVersion(ItemFileItem record) {
return null;
}
+ public static boolean updateDocumentText(ItemFileItem record, String documentText) {
+ try (Connection connection = DB.getConnection()) {
+ SqlUtils updateValues = new SqlUtils()
+ .add("document_text", documentText)
+ .add("processed", new Timestamp(System.currentTimeMillis()));
+ if (DB.update(connection, TABLE_NAME, updateValues, DB.WHERE("file_id = ?", record.getId()))) {
+ return true;
+ }
+ } catch (SQLException se) {
+ LOG.error("SQLException: " + se.getMessage());
+ }
+ LOG.error("The document text update failed!");
+ return false;
+ }
+
public static boolean remove(ItemFileItem record) {
try (Connection connection = DB.getConnection();
AutoStartTransaction a = new AutoStartTransaction(connection);
@@ -426,6 +501,10 @@ public static long findTotalFileSize() {
}
private static ItemFileItem buildRecord(ResultSet rs) {
+ return buildRecord(rs, false);
+ }
+
+ private static ItemFileItem buildRecord(ResultSet rs, boolean includeDocumentText) {
try {
ItemFileItem record = new ItemFileItem();
record.setId(rs.getLong("file_id"));
@@ -463,6 +542,9 @@ private static ItemFileItem buildRecord(ResultSet rs) {
record.setCategoryId(DB.getLong(rs, "category_id", -1L));
record.setWebPath(rs.getString("web_path"));
record.setTags(JsonCommand.fromJsonArray(rs.getString("tags")));
+ if (includeDocumentText) {
+ record.setDocumentText(rs.getString("document_text"));
+ }
return record;
} catch (SQLException se) {
LOG.error("buildRecord", se);
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java
index e4f4b24b..09406cf7 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemFileSpecification.java
@@ -37,12 +37,15 @@ public class ItemFileSpecification extends Entity {
private String barcode = null;
private long createdBy = -1;
private String[] fileType = null;
+ private String[] fileExtension = null;
private Long forUserId = -1L;
private String matchesName = null;
private String searchName = null;
private String searchContent = null;
+ private boolean includeDocumentText = false;
private int withinLastDays = -1;
private int inASubFolder = DataConstants.UNDEFINED;
+ private int isProcessed = DataConstants.UNDEFINED;
public ItemFileSpecification() {
}
@@ -131,6 +134,14 @@ public void setFileType(String value) {
}
}
+ public String[] getFileExtension() {
+ return fileExtension;
+ }
+
+ public void setFileExtension(String[] fileExtension) {
+ this.fileExtension = fileExtension;
+ }
+
public Long getForUserId() {
return forUserId;
}
@@ -163,6 +174,14 @@ public void setSearchContent(String searchContent) {
this.searchContent = searchContent;
}
+ public boolean getIncludeDocumentText() {
+ return includeDocumentText;
+ }
+
+ public void setIncludeDocumentText(boolean includeDocumentText) {
+ this.includeDocumentText = includeDocumentText;
+ }
+
public int getWithinLastDays() {
return withinLastDays;
}
@@ -182,4 +201,16 @@ public void setInASubFolder(int inASubFolder) {
public void setInASubFolder(boolean inASubFolder) {
this.inASubFolder = (inASubFolder ? DataConstants.TRUE : DataConstants.FALSE);
}
+
+ public int getIsProcessed() {
+ return isProcessed;
+ }
+
+ public void setIsProcessed(int isProcessed) {
+ this.isProcessed = isProcessed;
+ }
+
+ public void setIsProcessed(boolean isProcessed) {
+ this.isProcessed = (isProcessed ? DataConstants.TRUE : DataConstants.FALSE);
+ }
}
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java
index 9eb1fe46..b76750f4 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemRepository.java
@@ -580,6 +580,10 @@ private static DataResult query(ItemSpecification specification, DataConstraints
where.AND("tags", specification.getFilterTags(), SqlWhere.AND_OPERATOR);
}
+ if (specification.getExcludeTags() != null && specification.getExcludeTags().length > 0) {
+ where.AND("tags", specification.getExcludeTags(), SqlWhere.NOT_OR_OPERATOR);
+ }
+
if (specification.getFieldInFilters() != null) {
for (Map.Entry> entry : specification.getFieldInFilters().entrySet()) {
String fieldName = entry.getKey();
diff --git a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java
index 00620621..0efa56d7 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/persistence/items/ItemSpecification.java
@@ -56,6 +56,7 @@ public class ItemSpecification {
private long datasetId = -1L;
private Timestamp datasetSyncTimestampThreshold = null;
private String[] filterTags = null;
+ private String[] excludeTags = null;
private String[] regionTags = null;
private List customFieldFilters = null;
private Map> fieldInFilters = null;
@@ -266,6 +267,14 @@ public void setFilterTags(String[] filterTags) {
this.filterTags = filterTags;
}
+ public String[] getExcludeTags() {
+ return excludeTags;
+ }
+
+ public void setExcludeTags(String[] excludeTags) {
+ this.excludeTags = excludeTags;
+ }
+
public String[] getRegionTags() {
return regionTags;
}
diff --git a/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java b/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java
index 9425731b..9e741c66 100644
--- a/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java
+++ b/src/main/java/com/simisinc/platform/infrastructure/scheduler/SchedulerManager.java
@@ -42,7 +42,6 @@
import com.simisinc.platform.infrastructure.scheduler.analytics.RecordPerformanceMetricJob;
import com.simisinc.platform.infrastructure.scheduler.cms.LoadSystemFilesJob;
import com.simisinc.platform.infrastructure.scheduler.cms.RecordWebPageHitJob;
-import com.simisinc.platform.infrastructure.scheduler.cms.RefreshAllWebPageTextIndexesJob;
import com.simisinc.platform.infrastructure.scheduler.cms.WebPageHitSnapshotJob;
import com.simisinc.platform.infrastructure.scheduler.cms.WebPageHitsCleanupJob;
import com.simisinc.platform.infrastructure.scheduler.ecommerce.OrderManagementProcessNewOrders;
@@ -51,6 +50,8 @@
import com.simisinc.platform.infrastructure.scheduler.login.UserTokensCleanupJob;
import com.simisinc.platform.infrastructure.scheduler.medicine.ProcessMedicineSchedulesJob;
import com.simisinc.platform.infrastructure.scheduler.socialmedia.InstagramMediaSnapshotJob;
+import com.zeroio.platform.infrastructure.scheduler.cms.RefreshAllDocumentTextIndexesJob;
+import com.zeroio.platform.infrastructure.scheduler.cms.RefreshAllWebPageTextIndexesJob;
/**
* Initializes background jobs to be run on a schedule
@@ -80,6 +81,7 @@ public class SchedulerManager {
public static final String ORDER_MANAGEMENT_PROCESS_SHIPPING_UPDATES_JOB = "OrderManagementProcessShippingUpdates";
public static final String PROCESS_MEDICINE_SCHEDULES_JOB = "ProcessMedicineSchedules";
public static final String REFRESH_ALL_WEB_PAGE_TEXT_INDEXES_JOB = "RefreshAllWebPageTextIndexes";
+ public static final String REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB = "RefreshAllDocumentTextIndexes";
// Jobs which can be run by multiple clients
public static final String DATASETS_DOWNLOAD_AND_SYNC_JOB = "DatasetsDownloadAndSync";
@@ -164,6 +166,8 @@ public static void startup(ServletContext servletContext1) {
BackgroundJob.scheduleRecurrently(OAUTH_STATE_CLEANUP_JOB, Cron.every5minutes(), OAuthStateCleanupJob::execute);
BackgroundJob.scheduleRecurrently(REFRESH_ALL_WEB_PAGE_TEXT_INDEXES_JOB, Cron.yearly(month, day),
RefreshAllWebPageTextIndexesJob::execute);
+ BackgroundJob.scheduleRecurrently(REFRESH_ALL_DOCUMENT_TEXT_INDEXES_JOB, Cron.yearly(month, day),
+ RefreshAllDocumentTextIndexesJob::execute);
BackgroundJob.scheduleRecurrently(INSTAGRAM_MEDIA_SNAPSHOT_JOB, Cron.hourly(), InstagramMediaSnapshotJob::execute);
BackgroundJob.scheduleRecurrently(DATASETS_DOWNLOAD_AND_SYNC_JOB, Cron.minutely(), DatasetsDownloadAndSyncJob::execute);
BackgroundJob.scheduleRecurrently(ORDER_MANAGEMENT_PROCESS_NEW_ORDERS_JOB, Cron.minutely(),
diff --git a/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java b/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java
index e393af73..335e33c6 100644
--- a/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java
+++ b/src/main/java/com/simisinc/platform/presentation/controller/PageServlet.java
@@ -364,16 +364,9 @@ public void service(HttpServletRequest request, HttpServletResponse response) {
// Always access the WebPage record so it can be used downstream
WebPage webPage = LoadWebPageCommand.loadByLink(pageRequest.getPagePath());
+ boolean showArchivedMessage = false;
if (webPage != null) {
- // Determine if this is a draft page
- if (webPage.getDraft()) {
- if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) {
- LOG.error("DRAFT FOUND, no access: " + pageRequest.getPagePath() + " " + pageRequest.getRemoteAddr());
- controllerSession.clearAllWidgetData();
- response.sendError(HttpServletResponse.SC_NOT_FOUND);
- return;
- }
- }
+
// Determine if this is a redirect
String redirectLocation = webPage.getRedirectUrl();
if (StringUtils.isNotBlank(redirectLocation)) {
@@ -387,12 +380,44 @@ public void service(HttpServletRequest request, HttpServletResponse response) {
response.setStatus(SC_MOVED_PERMANENTLY);
return;
}
- // @todo Not needed once pageRenderInfo has calculated title, keywords, description
+
+ // Determine if this is a draft page (and unavailable to general users)
+ if (webPage.getDraft()) {
+ if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) {
+ LOG.debug("DRAFT FOUND, no access: " + pageRequest.getPagePath());
+ controllerSession.clearAllWidgetData();
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ }
+
+ // Determine if this page is archived
+ if (!webPage.isEnabled()) {
+ // Found an archived page, so for logged out users show a 404
+ if (!userSession.isLoggedIn()) {
+ LOG.debug("ARCHIVED FOUND, no access: " + pageRequest.getPagePath());
+ controllerSession.clearAllWidgetData();
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ // Found an archived page, so for normal users show a message instead of the page
+ if (!userSession.hasRole("admin") && !userSession.hasRole("content-manager")) {
+ showArchivedMessage = true;
+ }
+ }
+
+ // @todo Probably not needed once pageRenderInfo has calculated title, keywords, description
request.setAttribute(MASTER_WEB_PAGE, webPage);
}
// Determine the Page XML Layout for this request
- Page pageRef = WebPageXmlLayoutCommand.retrievePageForRequest(webPage, pageRequest.getPagePath());
+ Page pageRef;
+ if (showArchivedMessage) {
+ // Load archived page template for normal users
+ pageRef = WebPageXmlLayoutCommand.retrievePage("_page_archived_");
+ } else {
+ pageRef = WebPageXmlLayoutCommand.retrievePageForRequest(webPage, pageRequest.getPagePath());
+ }
// Load site properties early (needed for online/draft checks and hit tracking)
Map sitePropertyMap = LoadSitePropertyCommand.loadAsMap("site");
diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java
index 92ea628d..913f3b12 100644
--- a/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java
+++ b/src/main/java/com/simisinc/platform/presentation/widgets/admin/cms/WebPageFormWidget.java
@@ -181,12 +181,52 @@ public WidgetContext action(WidgetContext context) {
context.setRedirect(webPage.getLink());
String action = context.getParameter("action");
if ("deletePage".equals(action)) {
+ // Only admins can delete pages
+ if (!context.hasRole("admin")) {
+ context.setErrorMessage("You do not have permission to delete pages");
+ return context;
+ }
try {
WebPageRepository.remove(webPage);
context.setSuccessMessage("Page was deleted");
+ context.setRedirect(webPage.getLink());
} catch (Exception e) {
context.setErrorMessage("The page could not be deleted: " + e.getMessage());
}
+ } else if ("archivePage".equals(action)) {
+ // Content managers and admins can archive pages
+ if (!context.hasRole("admin") && !context.hasRole("content-manager")) {
+ context.setErrorMessage("You do not have permission to archive pages");
+ return context;
+ }
+ boolean success = WebPageRepository.archivePage(webPage, context.getUserId());
+ if (success) {
+ context.setWarningMessage("Page was archived");
+ // Stay on the form page
+ context.setRedirect(context.getUri() + "?webPageId=" + webPageId);
+ } else {
+ context.setErrorMessage("The page could not be archived");
+ // Redirect back to form on error
+ context.setRedirect(context.getUri() + "?webPageId=" + webPageId);
+ }
+ } else if ("unarchivePage".equals(action)) {
+ // Content managers and admins can unarchive pages
+ if (!context.hasRole("admin") && !context.hasRole("content-manager")) {
+ context.setErrorMessage("You do not have permission to unarchive pages");
+ return context;
+ }
+ boolean success = WebPageRepository.unarchivePage(webPage, context.getUserId());
+ if (success) {
+ context.setSuccessMessage("Page was unarchived");
+ // Stay on the form page
+ context.setRedirect(context.getUri() + "?webPageId=" + webPageId);
+ } else {
+ context.setErrorMessage("The page could not be unarchived");
+ // Redirect back to form on error
+ context.setRedirect(context.getUri() + "?webPageId=" + webPageId);
+ }
+ } else {
+ context.setRedirect(webPage.getLink());
}
return context;
}
diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java
index ed812f6c..d85522e9 100644
--- a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java
+++ b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemFieldsWidget.java
@@ -28,7 +28,7 @@
import com.simisinc.platform.presentation.widgets.cms.PreferenceEntriesList;
/**
- * Description
+ * Displays the fields for an item
*
* @author matt rajkowski
* @created 4/20/18 2:23 PM
@@ -47,15 +47,11 @@ public WidgetContext execute(WidgetContext context) {
return null;
}
- PreferenceEntriesList entriesList = context.getPreferenceAsDataList("fields");
- if (entriesList.isEmpty()) {
- return context;
- }
-
// Use the fields preference to determine the object properties to be shown
+ PreferenceEntriesList entriesList = context.getPreferenceAsDataList("fields");
List fieldList = ItemCustomFieldCommand.renderDisplayValues(entriesList, item);
- // Show the custom fields if requested
+ // Append the custom fields if requested
boolean showAllCustomFields = "true".equals(context.getPreferences().get("showAllCustomFields"));
if (showAllCustomFields && item.getCustomFieldList() != null && !item.getCustomFieldList().isEmpty()) {
fieldList.addAll(item.getCustomFieldList().values());
diff --git a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java
index 7f3f774e..32d21e5f 100644
--- a/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java
+++ b/src/main/java/com/simisinc/platform/presentation/widgets/items/ItemsListWidget.java
@@ -209,15 +209,23 @@ public WidgetContext execute(WidgetContext context) {
// Handle the case where the filter value is a reference to another field (e.g., ${item.custom.Code})
LOG.debug("Processing custom field filter: " + filterFieldName + " = " + filterFieldValue);
if (filterFieldValue.startsWith("${item.custom.") && filterFieldValue.endsWith("}")) {
- String referencedFieldName = filterFieldValue.substring("${item.custom.".length(), filterFieldValue.length() - 1);
+ String referencedFieldName = filterFieldValue.substring("${item.custom.".length(),
+ filterFieldValue.length() - 1);
+
// Load the authorized item that this widget is embedded on
Item currentItem = LoadItemCommand.loadItemByUniqueId(context.getCoreData().get("itemUniqueId"));
if (currentItem == null) {
// If the current item is not found, we cannot apply the filter, so we return null to indicate that the widget should not render any items
return null;
}
+
// Retrieve the value of the referenced custom field from the current item in the request context
- String referencedFieldValue = currentItem.getCustomField(referencedFieldName).getValue();
+ CustomField referencedField = currentItem.getCustomField(referencedFieldName);
+ if (referencedField == null) {
+ // If the referenced field is not found, we cannot apply the filter, so we return null to indicate that the widget should not render any items
+ return null;
+ }
+ String referencedFieldValue = referencedField.getValue();
if (StringUtils.isBlank(referencedFieldValue)) {
// If the referenced field value is blank, we cannot apply the filter, so we return null to indicate that the widget should not render any items
return null;
diff --git a/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java b/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java
index c22193c0..a35da618 100644
--- a/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java
+++ b/src/main/java/com/simisinc/platform/rest/controller/RestRequestFilter.java
@@ -251,6 +251,12 @@ public void doFilter(ServletRequest request, ServletResponse servletResponse, Fi
return;
}
+ // At this point a guest may only use GET or HEAD
+ if (!"get".equals(requestMethod) && !"head".equals(requestMethod)) {
+ doUnauthorized(servletResponse);
+ return;
+ }
+
// Limit the number of hits per minute based on the successful use of the api key
if (!isLocal && !RateLimitCommand.isAppAllowedRightNow(thisApp, useLenientRateLimit)) {
do429(servletResponse);
diff --git a/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java b/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java
index 2833f237..24d01399 100644
--- a/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java
+++ b/src/main/java/com/simisinc/platform/rest/controller/RestServlet.java
@@ -23,6 +23,8 @@
import java.io.PrintWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -63,6 +65,20 @@ public class RestServlet extends HttpServlet {
// Services Cache
private Map serviceInstances = new ConcurrentHashMap();
+ private static class RouteMatch {
+ private final Object classRef;
+ private final String endpoint;
+ private final String pathParam;
+ private final String pathParam2;
+
+ private RouteMatch(Object classRef, String endpoint, String pathParam, String pathParam2) {
+ this.classRef = classRef;
+ this.endpoint = endpoint;
+ this.pathParam = pathParam;
+ this.pathParam2 = pathParam2;
+ }
+ }
+
@Override
public void init(ServletConfig config) throws ServletException {
@@ -111,6 +127,7 @@ public void service(HttpServletRequest request, HttpServletResponse response) {
long startRequestTime = System.currentTimeMillis();
+ LOG.debug("-----------------------------------------------------------------------");
LOG.debug("Service processor...");
// Determine request values
@@ -143,21 +160,11 @@ public void service(HttpServletRequest request, HttpServletResponse response) {
// Determine the resource
try {
// Get the cached class reference for processing
- Object classRef = serviceInstances.get(endpoint);
- String pathEndpoint = null;
- if (classRef == null) {
- LOG.debug("Could not find endpoint: " + endpoint);
- if (endpoint.contains("/")) {
- // Try as a pathParam
- pathEndpoint = endpoint.substring(0, endpoint.indexOf("/"));
- pathParam = endpoint.substring(endpoint.indexOf("/") + 1);
- if (pathParam.contains("/")) {
- pathParam2 = pathParam.substring(pathParam.indexOf("/") + 1);
- pathParam = pathParam.substring(0, pathParam.indexOf("/"));
- }
- classRef = serviceInstances.get(pathEndpoint);
- }
- }
+ RouteMatch routeMatch = findRoute(endpoint);
+ Object classRef = (routeMatch != null ? routeMatch.classRef : null);
+ String pathEndpoint = (routeMatch != null ? routeMatch.endpoint : null);
+ pathParam = (routeMatch != null ? routeMatch.pathParam : null);
+ pathParam2 = (routeMatch != null ? routeMatch.pathParam2 : null);
if (classRef == null) {
LOG.error("Class not found for service: " + endpoint);
sendError(response, SC_NOT_FOUND, "Endpoint not found");
@@ -187,13 +194,8 @@ public void service(HttpServletRequest request, HttpServletResponse response) {
// Execute the service
ServiceResponse result = null;
try {
- LOG.debug("-----------------------------------------------------------------------");
- if (pathEndpoint != null) {
- LOG.debug("Executing service: " + pathEndpoint);
- } else {
- LOG.debug("Executing service: " + endpoint);
- }
RestService service = (RestService) classRef;
+ LOG.debug("Executing service: " + service.getClass().getName());
if ("post".equals(requestMethod)) {
result = service.post(serviceContext);
} else if ("put".equals(requestMethod)) {
@@ -260,4 +262,120 @@ public static void sendError(HttpServletResponse response, int code, String erro
out.flush();
}
}
+
+ private RouteMatch findRoute(String requestEndpoint) {
+ if (requestEndpoint != null && requestEndpoint.contains("?")) {
+ requestEndpoint = requestEndpoint.substring(0, requestEndpoint.indexOf("?"));
+ }
+ Object classRef = serviceInstances.get(requestEndpoint);
+ if (classRef != null) {
+ return new RouteMatch(classRef, requestEndpoint, null, null);
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Could not find exact endpoint: " + requestEndpoint + ", trying path pattern match");
+ }
+
+ List requestParts = splitPath(requestEndpoint);
+ RouteMatch bestExactPatternMatch = null;
+ RouteMatch bestPrefixPatternMatch = null;
+ for (Map.Entry entry : serviceInstances.entrySet()) {
+ String endpointPattern = entry.getKey();
+ if (!endpointPattern.contains("{")) {
+ continue;
+ }
+
+ List patternParts = splitPath(endpointPattern);
+ if (patternParts.size() > requestParts.size()) {
+ continue;
+ }
+
+ RouteMatch routeMatch = matchPattern(entry.getValue(), endpointPattern, patternParts, requestParts);
+ if (routeMatch == null) {
+ continue;
+ }
+
+ if (patternParts.size() == requestParts.size()) {
+ if (bestExactPatternMatch == null || splitPath(bestExactPatternMatch.endpoint).size() < patternParts.size()) {
+ bestExactPatternMatch = routeMatch;
+ }
+ } else if (bestPrefixPatternMatch == null
+ || splitPath(bestPrefixPatternMatch.endpoint).size() < patternParts.size()) {
+ bestPrefixPatternMatch = routeMatch;
+ }
+ }
+
+ if (bestExactPatternMatch != null) {
+ return bestExactPatternMatch;
+ }
+ if (bestPrefixPatternMatch != null) {
+ return bestPrefixPatternMatch;
+ }
+
+ String bestLiteralPrefix = null;
+ for (String endpoint : serviceInstances.keySet()) {
+ if (endpoint.contains("{")) {
+ continue;
+ }
+ if (requestEndpoint.equals(endpoint) || requestEndpoint.startsWith(endpoint + "/")) {
+ if (bestLiteralPrefix == null || splitPath(bestLiteralPrefix).size() < splitPath(endpoint).size()) {
+ bestLiteralPrefix = endpoint;
+ }
+ }
+ }
+ if (bestLiteralPrefix != null) {
+ return new RouteMatch(serviceInstances.get(bestLiteralPrefix), bestLiteralPrefix, null, null);
+ }
+ return null;
+ }
+
+ private RouteMatch matchPattern(Object classRef, String endpointPattern, List patternParts,
+ List requestParts) {
+ String matchedPathParam = null;
+ String matchedPathParam2 = null;
+ int matchedParamCount = 0;
+
+ for (int i = 0; i < patternParts.size(); i++) {
+ String patternPart = patternParts.get(i);
+ String requestPart = requestParts.get(i);
+
+ if (patternPart.startsWith("{") && patternPart.endsWith("}")) {
+ matchedParamCount++;
+ if (matchedParamCount == 1) {
+ matchedPathParam = requestPart;
+ } else if (matchedParamCount == 2) {
+ matchedPathParam2 = requestPart;
+ }
+ continue;
+ }
+
+ if (!patternPart.equals(requestPart)) {
+ return null;
+ }
+ }
+
+ if (requestParts.size() > patternParts.size()) {
+ if (matchedPathParam2 == null && patternParts.size() == 1 && !requestParts.isEmpty()) {
+ matchedPathParam = requestParts.get(0);
+ matchedPathParam2 = requestParts.get(1);
+ } else if (matchedPathParam2 == null && patternParts.size() < requestParts.size()) {
+ matchedPathParam2 = requestParts.get(patternParts.size());
+ }
+ }
+
+ return new RouteMatch(classRef, endpointPattern, matchedPathParam, matchedPathParam2);
+ }
+
+ private List splitPath(String value) {
+ List parts = new ArrayList<>();
+ if (StringUtils.isBlank(value)) {
+ return parts;
+ }
+ for (String part : value.split("/")) {
+ if (StringUtils.isNotBlank(part)) {
+ parts.add(part);
+ }
+ }
+ return parts;
+ }
}
diff --git a/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java b/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java
index c0ff6be8..b4d37623 100644
--- a/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java
+++ b/src/main/java/com/simisinc/platform/rest/controller/ServiceContext.java
@@ -1,4 +1,5 @@
/*
+ * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski)
* Copyright 2022 SimIS Inc. (https://www.simiscms.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,14 +17,16 @@
package com.simisinc.platform.rest.controller;
-import com.simisinc.platform.domain.model.App;
-import com.simisinc.platform.domain.model.User;
-import org.apache.commons.lang3.StringUtils;
+import java.io.Serializable;
+import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import java.io.Serializable;
-import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.simisinc.platform.domain.model.App;
+import com.simisinc.platform.domain.model.User;
/**
* Common object for context objects available during a service request
@@ -155,6 +158,10 @@ public int getParameterAsInt(String name, int defaultValue) {
return defaultValue;
}
+ public String[] getParameterAsArray(String name) {
+ return parameterMap.get(name);
+ }
+
public String getPathParam2() {
return pathParam2;
}
diff --git a/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java b/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java
index a719e59c..81983cd4 100644
--- a/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java
+++ b/src/main/java/com/simisinc/platform/rest/controller/XMLServiceLoader.java
@@ -1,4 +1,5 @@
/*
+ * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski)
* Copyright 2022 SimIS Inc. (https://www.simiscms.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -87,12 +88,12 @@ private void addServices(Document document) {
NodeList objectTags = document.getElementsByTagName("service");
for (int i = 0; i < objectTags.getLength(); i++) {
Element objectTag = (Element) objectTags.item(i);
- String endpoint = objectTag.getAttribute("endpoint");
String endpointValue = objectTag.getAttribute("endpoint");
+ String endpoint = endpointValue;
String serviceClass = objectTag.getAttribute("class");
String method = objectTag.getAttribute("method");
- if (endpoint.contains("/{")) {
- endpoint = endpoint.substring(0, endpoint.indexOf("/{"));
+ if (endpoint.contains("?")) {
+ endpoint = endpoint.substring(0, endpoint.indexOf("?"));
}
Map service = new HashMap<>();
service.put("endpoint", endpoint);
diff --git a/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java b/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java
new file mode 100644
index 00000000..dba05746
--- /dev/null
+++ b/src/main/java/com/simisinc/platform/rest/services/items/ItemFilesService.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2026 Matt Rajkowski (https://github.com/rajkowski)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.simisinc.platform.rest.services.items;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.simisinc.platform.application.items.LoadCollectionCommand;
+import com.simisinc.platform.application.items.LoadItemCommand;
+import com.simisinc.platform.domain.model.items.Collection;
+import com.simisinc.platform.domain.model.items.Item;
+import com.simisinc.platform.domain.model.items.ItemFileItem;
+import com.simisinc.platform.infrastructure.database.DataConstraints;
+import com.simisinc.platform.infrastructure.persistence.items.ItemFileItemRepository;
+import com.simisinc.platform.infrastructure.persistence.items.ItemFileSpecification;
+import com.simisinc.platform.rest.controller.GenericRestService;
+import com.simisinc.platform.rest.controller.ServiceContext;
+import com.simisinc.platform.rest.controller.ServiceResponse;
+import com.simisinc.platform.rest.controller.ServiceResponseCommand;
+
+/**
+ * Returns a list of item files
+ *
+ * @author matt rajkowski
+ * @created 7/28/26 10:15 AM
+ */
+public class ItemFilesService extends GenericRestService {
+
+ private static Log LOG = LogFactory.getLog(ItemFilesService.class);
+
+ private static final String EXPAND_DOCUMENT_TEXT = "documentText";
+
+ // GET /item/{itemUniqueId}/files
+ @Override
+ public ServiceResponse get(ServiceContext context) {
+
+ // Determine the item
+ String itemUniqueId = context.getPathParam();
+ Item item = LoadItemCommand.loadItemByUniqueIdForAuthorizedUser(itemUniqueId, context.getUserId());
+ if (item == null) {
+ ServiceResponse response = new ServiceResponse(404);
+ response.getError().put("title", "Item was not found");
+ return response;
+ }
+
+ // Validate access to the collection
+ Collection collection = LoadCollectionCommand.loadCollectionByIdForAuthorizedUser(item.getCollectionId(),
+ context.getUserId());
+ if (collection == null) {
+ LOG.warn("User does not have access to this collection");
+ ServiceResponse response = new ServiceResponse(400);
+ response.getError().put("title", "Item was not found");
+ return response;
+ }
+
+ // Return the ItemDetailResponse if requested
+ boolean expandDocumentText = false;
+ String[] expand = context.getParameterAsArray("expand");
+ if (expand != null) {
+ for (String expandValue : expand) {
+ if (EXPAND_DOCUMENT_TEXT.equals(expandValue)) {
+ expandDocumentText = true;
+ }
+ }
+ }
+
+ // Retrieve all files for the item within user visibility
+ ItemFileSpecification specification = new ItemFileSpecification();
+ specification.setItemId(item.getId());
+ specification.setForUserId(context.getUserId());
+ specification.setIncludeDocumentText(expandDocumentText);
+
+ // Enable paging
+ int pageNumber = context.getParameterAsInt("page", 1);
+ int pageSize = context.getParameterAsInt("size", 50);
+
+ DataConstraints constraints = new DataConstraints(pageNumber, pageSize);
+ constraints.setColumnToSortBy("created", "desc");
+
+ List itemFileList = ItemFileItemRepository.findAll(specification, constraints);
+ List