Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '25-ea'
java-version: '25'
distribution: 'temurin'
cache: maven

Expand Down
1 change: 1 addition & 0 deletions cms-api/src/main/java/com/condation/cms/api/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

/**
*
* @author t.marx

Check warning on line 33 in cms-api/src/main/java/com/condation/cms/api/Constants.java

View workflow job for this annotation

GitHub Actions / build

no main description
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class Constants {
Expand Down Expand Up @@ -70,6 +70,7 @@
public static final String ALIASES_REDIRECT = "aliases_redirect";

public static final String TRANSLATIONS = "translations";
public static final String URL = "url";
}

public static class Folders {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

/**
*
* @author t.marx

Check warning on line 30 in cms-api/src/main/java/com/condation/cms/api/SiteProperties.java

View workflow job for this annotation

GitHub Actions / build

no main description
*/
public interface SiteProperties {

Expand All @@ -47,6 +47,7 @@

public String theme ();

@Deprecated(since = "8.3.0")
public String queryIndexMode ();

public Locale locale ();
Expand Down
9 changes: 7 additions & 2 deletions cms-api/src/main/java/com/condation/cms/api/db/Content.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ public interface Content {

List<ContentNode> listDirectories(final ReadOnlyFile base, final String start);

@Deprecated(since = "8.3.0")
Optional<ContentNode> byUri (final String uri);

Optional<Map<String,Object>> getMeta(final String uri);

Optional<ContentNode> byPath (final String path);

Optional<ContentNode> byUrl (final String url);

Optional<Map<String,Object>> getMeta(final String path);

public <T> ContentQuery<T> query(final BiFunction<ContentNode, Integer, T> nodeMapper);

Expand Down
23 changes: 14 additions & 9 deletions cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.condation.cms.api.request.RequestContext;
import com.condation.cms.api.request.RequestContextScope;
import com.condation.cms.api.utils.MapUtil;
import com.condation.cms.api.utils.PathUtil;
import com.condation.cms.api.utils.SectionUtil;
import com.google.common.math.DoubleMath;
import java.io.Serializable;
Expand All @@ -38,23 +39,27 @@
*
* @author t.marx
*/
public record ContentNode(String uri, String name, Map<String, Object> data,
public record ContentNode(String uri, String url, String name, Map<String, Object> data,
boolean directory, Map<String, ContentNode> children, LocalDate lastmodified) implements Serializable {

public ContentNode(String uri, String name, Map<String, Object> data, boolean directory, Map<String, ContentNode> children) {
this(uri, name, data, directory, children, LocalDate.now());
public ContentNode(String uri, String url, String name, Map<String, Object> data, boolean directory, Map<String, ContentNode> children) {
this(uri, url, name, data, directory, children, LocalDate.now());
}

public ContentNode(String uri, String name, Map<String, Object> data, boolean directory) {
this(uri, name, data, directory, new HashMap<>(), LocalDate.now());
public ContentNode(String uri, String url, String name, Map<String, Object> data, boolean directory) {
this(uri, url, name, data, directory, new HashMap<>(), LocalDate.now());
}

public ContentNode(String uri, String name, Map<String, Object> data) {
this(uri, name, data, false, new HashMap<>(), LocalDate.now());
public ContentNode(String uri, String url, String name, Map<String, Object> data) {
this(uri, url, name, data, false, new HashMap<>(), LocalDate.now());
}

public ContentNode(String uri, String name, Map<String, Object> data, LocalDate lastmodified) {
this(uri, name, data, false, new HashMap<>(), lastmodified);
public ContentNode(String uri, String url, String name, Map<String, Object> data, LocalDate lastmodified) {
this(uri, url, name, data, false, new HashMap<>(), lastmodified);
}

public String path () {
return uri;
}

public String nodeType() {
Expand Down Expand Up @@ -98,7 +103,7 @@
}

/**
* @deprecated use {@link WFStatusProvider#isPublished(ContentNode)} or

Check failure on line 106 in cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java

View workflow job for this annotation

GitHub Actions / build

reference not found

Check failure on line 106 in cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java

View workflow job for this annotation

GitHub Actions / build

reference not found
* {@link NodeVisibility#isVisible(ContentNode)} depending on intent.
*/
@Deprecated(since = "8.3.0", forRemoval = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import com.condation.cms.api.request.RequestContext;
import com.condation.cms.api.utils.HTTPUtil;
import com.condation.cms.api.utils.NodeUtil;
import com.condation.cms.api.utils.PathUtil;
import java.io.IOException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -67,10 +66,9 @@ public ListNode toListNode(final ContentNode node, final RequestContext context,

var name = NodeUtil.getName(node);
final ReadOnlyFile contentBase = db.getFileSystem().contentBase();
var temp_path = contentBase.resolve(node.uri());
var url = PathUtil.toURL(temp_path, contentBase);
var temp_path = contentBase.resolve(node.path());

url = HTTPUtil.modifyUrl(url, context);
var url = HTTPUtil.modifyUrl(node.url(), context);

var md = parse(temp_path);
var excerpt = NodeUtil.excerpt(node, md.get().content(), excerptLength, context.get(MarkdownRendererFeature.class).markdownRenderer());
Expand Down
37 changes: 35 additions & 2 deletions cms-api/src/main/java/com/condation/cms/api/utils/PathUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static String toRelativePath(final Path contentPath, final Path contentBa
if (!Files.isDirectory(contentPath)) {
tempPath = contentPath.getParent();
}
Path relativize = contentBase.relativize(tempPath);
Path relativize = normalizedAbsolute(contentBase).relativize(normalizedAbsolute(tempPath));
var uri = relativize.toString();
uri = uri.replaceAll("\\\\", "/");
return uri;
Expand All @@ -70,7 +70,7 @@ public static String toRelativePath(final ReadOnlyFile contentPath, final ReadOn
}

public static String toRelativeFile(final Path contentFile, final Path contentBase) {
Path relativize = contentBase.relativize(contentFile);
Path relativize = normalizedAbsolute(contentBase).relativize(normalizedAbsolute(contentFile));
if (Files.isDirectory(contentFile)) {
relativize = relativize.resolve("index.md");
}
Expand All @@ -79,6 +79,21 @@ public static String toRelativeFile(final Path contentFile, final Path contentBa
return uri;
}

/**
* Relativizes a file system entry without accessing the entry itself. This
* is required for delete events, where the path no longer exists.
*/
public static String toRelativeEntry(final Path entry, final Path contentBase) {
return normalizedAbsolute(contentBase)
.relativize(normalizedAbsolute(entry))
.toString()
.replace('\\', '/');
}

private static Path normalizedAbsolute(Path path) {
return path.toAbsolutePath().normalize();
}

public static String toRelativeFile(ReadOnlyFile contentFile, final ReadOnlyFile contentBase) {
if (contentFile.isDirectory()) {
contentFile = contentFile.resolve("index.md");
Expand Down Expand Up @@ -119,4 +134,22 @@ public static String toURL (String relFile) {

return relFile;
}

public static String normalizeURL(final String url) {
if (url == null || url.isBlank()) {
return "/";
}

var normalized = url.trim().replace('\\', '/');
if (!normalized.startsWith("/")) {
normalized = "/" + normalized;
}
while (normalized.contains("//")) {
normalized = normalized.replace("//", "/");
}
while (normalized.length() > 1 && normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class ContentNodeTest {

@Test
public void test_publish() {
var contentNode = new ContentNode("", "", Map.of());
var contentNode = new ContentNode("", "", "", Map.of());
Assertions.assertThat(NodeVisibility.isVisible(contentNode)).isFalse();
Assertions.assertThat(contentNode.isVisible()).isFalse();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
*
Expand Down Expand Up @@ -61,5 +62,31 @@ public void test_to_url() {
toURI = PathUtil.toURL(contentBase.resolve(""), contentBase);
assertThat(toURI).isEqualTo("/");
}

@Test
public void test_normalize_url() {
assertThat(PathUtil.normalizeURL(null)).isEqualTo("/");
assertThat(PathUtil.normalizeURL("shop//item/")).isEqualTo("/shop/item");
assertThat(PathUtil.normalizeURL("/shop/item")).isEqualTo("/shop/item");
}

@Test
public void relativeEntryDoesNotNeedToExist(@TempDir Path tempDirectory) {
var deletedEntry = tempDirectory.resolve("old/sub/page.md");

assertThat(PathUtil.toRelativeEntry(deletedEntry, tempDirectory))
.isEqualTo("old/sub/page.md");
}

@Test
public void relativePathsAcceptAbsoluteEntryAndRelativeBase() {
var relativeBase = Path.of("target", "mixed-paths");
var absoluteFile = relativeBase.resolve("sections/page.md").toAbsolutePath().normalize();

assertThat(PathUtil.toRelativeFile(absoluteFile, relativeBase))
.isEqualTo("sections/page.md");
assertThat(PathUtil.toRelativePath(absoluteFile, relativeBase))
.isEqualTo("sections");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class DefaultWFStatusProviderTest {
public void test_publish_date_1_11_2023() {
var cal = Calendar.getInstance();
cal.set(2023, 11, 1);
var contentNode = new ContentNode("", "", Map.of(
var contentNode = new ContentNode("", "", "", Map.of(
Constants.MetaFields.PUBLISH_DATE, cal.getTime(),
Constants.MetaFields.STATUS, DefaultWFStatusProvider.STATUS_PUBLISHED
));
Expand All @@ -53,7 +53,7 @@ public void test_publish_date_1_11_2023() {
public void test_publish_date_1_11_2123() {
var cal = Calendar.getInstance();
cal.set(2123, 11, 1);
var contentNode = new ContentNode("", "", Map.of(
var contentNode = new ContentNode("", "", "", Map.of(
Constants.MetaFields.PUBLISH_DATE, cal.getTime(),
Constants.MetaFields.STATUS, DefaultWFStatusProvider.STATUS_PUBLISHED
));
Expand All @@ -64,7 +64,7 @@ public void test_publish_date_1_11_2123() {
public void test_unpublish_date_1_11_2023() {
var cal = Calendar.getInstance();
cal.set(2023, 11, 1);
var contentNode = new ContentNode("", "", Map.of(
var contentNode = new ContentNode("", "", "", Map.of(
Constants.MetaFields.UNPUBLISH_DATE, cal.getTime(),
Constants.MetaFields.STATUS, DefaultWFStatusProvider.STATUS_PUBLISHED
));
Expand All @@ -75,7 +75,7 @@ public void test_unpublish_date_1_11_2023() {
public void test_unpublish_date_1_11_2123() {
var cal = Calendar.getInstance();
cal.set(2123, 11, 1);
var contentNode = new ContentNode("", "", Map.of(
var contentNode = new ContentNode("", "", "", Map.of(
Constants.MetaFields.UNPUBLISH_DATE, cal.getTime(),
Constants.MetaFields.STATUS, DefaultWFStatusProvider.STATUS_PUBLISHED
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ void setup() {

@Test
void simple_wf_test () {
ContentNode node = new ContentNode("/", "Node", new HashMap<>());
ContentNode node = new ContentNode("/", "/", "Node", new HashMap<>());

var transitions = wf.getNextTransitions(node);

Expand All @@ -84,7 +84,7 @@ void simple_wf_test () {

@Test
void transit_with_unknown_id_throws() {
ContentNode node = new ContentNode("/", "Node", new HashMap<>());
ContentNode node = new ContentNode("/", "/", "Node", new HashMap<>());

Assertions.assertThatThrownBy(() -> wf.transit("nonexistent", node))
.isInstanceOf(WFTransitionException.class)
Expand All @@ -94,7 +94,7 @@ void transit_with_unknown_id_throws() {
@Test
void transit_blocked_by_guard_throws() {
// node is in draft — "unpublish" guard requires published status
ContentNode node = new ContentNode("/", "Node", new HashMap<>());
ContentNode node = new ContentNode("/", "/", "Node", new HashMap<>());

Assertions.assertThatThrownBy(() -> wf.transit("unpublish", node))
.isInstanceOf(WFTransitionException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@
import com.condation.cms.api.content.RedirectContentResponse;
import com.condation.cms.api.db.ContentNode;
import com.condation.cms.api.db.DB;
import com.condation.cms.api.db.cms.ReadOnlyFile;
import com.condation.cms.api.feature.features.CurrentNodeFeature;
import com.condation.cms.api.feature.features.RequestFeature;
import com.condation.cms.api.request.RequestContext;
import com.condation.cms.api.utils.HTTPUtil;
import com.condation.cms.api.utils.PathUtil;
import com.condation.cms.core.content.ContentResolvingStrategy;
import com.google.common.base.Strings;
import java.io.IOException;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -63,55 +60,48 @@ public Optional<ContentResponse> getErrorContent (final RequestContext context)
}

private Optional<ContentResponse> getContent(final RequestContext context, boolean checkVisibility) {
var contentBase = db.getFileSystem().contentBase();
var path = ContentResolvingStrategy.uriToPath(context.get(RequestFeature.class).uri());
Optional<ReadOnlyFile> contentFileOpt = ContentResolvingStrategy.resolve(context.get(RequestFeature.class).uri(), db);
ReadOnlyFile contentFile = contentFileOpt.orElse(null);
final String uri = context.get(RequestFeature.class).uri();
var path = ContentResolvingStrategy.uriToPath(uri);

Optional<ContentNode> contentNodeOpt = db.getContent().byUrl(uri);

// handle alias
ContentNode contentNode = null;
boolean aliasRedirect = false;
if (contentFile == null || !contentFile.exists()) {
Optional<String> aliasRedirectUrl = Optional.empty();
if (contentNodeOpt.isEmpty()) {
var query = db.getContent().query((node, count) -> node);
var result = query.whereContains(Constants.MetaFields.ALIASES, "/" + path).get();
if (!result.isEmpty()) {
contentNode = result.getFirst();
contentFile = contentBase.resolve(contentNode.uri());
aliasRedirect = true;
aliasRedirectUrl = Optional.of(contentNode.url());
}
} else {
var uri = PathUtil.toRelativeFile(contentFile, contentBase);
final Optional<ContentNode> nodeByUri = db.getContent().byUri(uri);
if (nodeByUri.isPresent()) {
contentNode = nodeByUri.get();
}
contentNode = contentNodeOpt.get();
}

if (contentNode == null) {
return Optional.empty();
}

var contentFile = db.getFileSystem().contentBase().resolve(contentNode.path());

if (checkVisibility && !db.getContent().isVisible(contentNode)) {
return Optional.empty();
}


if (contentNode.isRedirect()) {
return Optional.of(new DefaultContentResponse(contentNode));
} else if (!Constants.NodeType.PAGE.equals(contentNode.nodeType())) {
return Optional.empty();
}
context.add(CurrentNodeFeature.class, new CurrentNodeFeature(contentNode));

if (contentNode.isRedirect()) {
return Optional.of(new RedirectContentResponse(contentNode.getRedirectLocation(), contentNode.getRedirectStatus()));
} else if (aliasRedirect) {
} else if (aliasRedirectUrl.isPresent()) {
var doRedirect = contentNode.getMetaValue(Constants.MetaFields.ALIASES_REDIRECT, true);
if (doRedirect) {
var url = PathUtil.toURL(contentFile, contentBase);
url = HTTPUtil.modifyUrl(url, context);
var url = HTTPUtil.modifyUrl(aliasRedirectUrl.get(), context);
return Optional.of(new RedirectContentResponse(url, 301));
}
} else if (!Constants.NodeType.PAGE.equals(contentNode.nodeType())) {
return Optional.empty();
}
context.add(CurrentNodeFeature.class, new CurrentNodeFeature(contentNode));

try {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.condation.cms.api.SiteProperties;
import com.condation.cms.api.db.ContentNode;
import com.condation.cms.api.utils.HTTPUtil;
import com.condation.cms.api.utils.PathUtil;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -43,7 +42,7 @@ public static Map<String, Object> createNodeProperties (ContentNode node, SitePr

Map<String, Object> properties = new HashMap<>();

var canonicalUrl = PathUtil.toURL(node .uri());
var canonicalUrl = node.url();
canonicalUrl = HTTPUtil.prependContext(canonicalUrl, siteProperties);

properties.put("url", canonicalUrl);
Expand Down
Loading
Loading