Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
[![Quality gate status](https://sonarcloud.io/api/project_badges/measure?project=CondationCMS_cms-server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=CondationCMS_cms-server)

![Maven Central Version](https://img.shields.io/maven-central/v/com.condation.cms/cms-api)

![NPM Version](https://img.shields.io/npm/v/condation-cms-ui)


# CondationCMS


CondationCMS is a fast, flexible, and developer-friendly content management system built with Java.

Content is stored in Markdown files with YAML front matter instead of a database. The file and directory structure remains transparent, easy to version with Git, and directly represents the structure of the website.
Expand Down Expand Up @@ -89,6 +97,15 @@ http://localhost:2020/manager/login

Sign in with the Manager user created in the previous step.

### Rebuild a site's metadata indexes

With the server running, rebuild the content and collection metadata indexes
for a site after changing its index configuration:

```bash
./server.sh host reindex <site-id>
```

## Documentation

Detailed information about installing, configuring, using, and extending CondationCMS is available in the official [CondationCMS documentation](https://condation.com/documentation).
Expand All @@ -99,4 +116,4 @@ CondationCMS is licensed under the [GNU Affero General Public License v3.0](./LI

Modules, plugins, themes, and extensions that use only the public and documented extension APIs may be distributed under a different license, including proprietary or commercial licenses.

See [LICENSE-EXCEPTION.md](./LICENSE-EXCEPTION.md) for details.
See [LICENSE-EXCEPTION.md](./LICENSE-EXCEPTION.md) for details.
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 @@ -75,6 +75,7 @@ public static class MetaFields {

public static class Folders {
public static final String CONTENT = "content/";
public static final String COLLECTIONS = "collections/";
public static final String TEMPLATES = "templates/";
public static final String ASSETS = "assets/";
public static final String EXTENSIONS = "extensions/";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.condation.cms.api.configuration.configs;

/*-
* #%L
* CMS Api
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* 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 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 <http://www.gnu.org/licenses/>.
* #L%
*/

import com.condation.cms.api.configuration.Config;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
* Reloadable, site-scoped collection definitions.
*/
public class CollectionConfiguration implements Config {

private volatile Map<String, CollectionDefinition> collections;

public CollectionConfiguration(Map<String, CollectionDefinition> collections) {
replaceCollections(collections);
}

/** Atomically replaces the complete set of collection definitions. */
public void replaceCollections(Map<String, CollectionDefinition> collections) {
this.collections = Map.copyOf(Objects.requireNonNull(collections));
}

public Optional<CollectionDefinition> collection(String name) {
return Optional.ofNullable(collections.get(name));
}

public Map<String, CollectionDefinition> collections() {
return collections;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.condation.cms.api.configuration.configs;

/*-
* #%L
* CMS Api
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* 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 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 <http://www.gnu.org/licenses/>.
* #L%
*/

import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

/**
* Configuration of one named collection.
*/
public record CollectionDefinition(String name, String site, CollectionDetailConfiguration detail) {

private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*");

public CollectionDefinition {
Objects.requireNonNull(name, "collection name must not be null");
if (!VALID_NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException("invalid collection name: " + name);
}
if (site != null) {
site = site.trim();
if (site.isEmpty()) {
throw new IllegalArgumentException("collection site must not be blank");
}
}
}

public CollectionDefinition(String name, CollectionDetailConfiguration detail) {
this(name, null, detail);
}

public Optional<CollectionDetailConfiguration> detailPage() {
return Optional.ofNullable(detail);
}

/** Site whose collection data should be used, or empty for a local collection. */
public Optional<String> sourceSite() {
return Optional.ofNullable(site);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.condation.cms.api.configuration.configs;

/*-
* #%L
* CMS Api
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* 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 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 <http://www.gnu.org/licenses/>.
* #L%
*/

import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;

/**
* Route and template used for collection detail pages.
*/
public record CollectionDetailConfiguration(
String route,
String template,
Map<String, Map<String, String>> mappings) {

private static final Pattern PARAMETER = Pattern.compile(
"\\{([a-zA-Z][a-zA-Z0-9_.-]*)(?::([^{}]*))?}");

public CollectionDetailConfiguration {
Objects.requireNonNull(route, "collection detail route must not be null");
Objects.requireNonNull(template, "collection detail template must not be null");

route = normalizeRoute(route);
template = template.trim();
if (template.isEmpty()) {
throw new IllegalArgumentException("collection detail template must not be blank");
}

var copiedMappings = new LinkedHashMap<String, Map<String, String>>();
Objects.requireNonNull(mappings, "collection detail mappings must not be null")
.forEach((field, values) -> copiedMappings.put(field, Map.copyOf(values)));
mappings = Map.copyOf(copiedMappings);

var parameters = parameters(route);
if (parameters.isEmpty()) {
throw new IllegalArgumentException("collection detail route must contain at least one parameter");
}
var matcher = PARAMETER.matcher(route);
var formattedFields = new HashSet<String>();
while (matcher.find()) {
var format = matcher.group(2);
if (format == null) {
continue;
}
if (format.isBlank()) {
throw new IllegalArgumentException("collection detail date format must not be blank");
}
if (format.contains("/")) {
throw new IllegalArgumentException("collection detail date format must not contain '/': " + format);
}
DateTimeFormatter.ofPattern(format);
formattedFields.add(matcher.group(1));
}
for (var field : mappings.keySet()) {
if (!parameters.contains(field)) {
throw new IllegalArgumentException("mapping references no route parameter: " + field);
}
if (formattedFields.contains(field)) {
throw new IllegalArgumentException("route parameter cannot have both a format and a mapping: " + field);
}
}
}

public CollectionDetailConfiguration(String route, String template) {
this(route, template, Map.of());
}

public String parameter() {
return parameters().getFirst();
}

public List<String> parameters() {
return parameters(route);
}

public int parameterOccurrences() {
return (int) PARAMETER.matcher(route).results().count();
}

public boolean hasFormats() {
return PARAMETER.matcher(route).results().anyMatch(match -> match.group(2) != null);
}

private static List<String> parameters(String route) {
return PARAMETER.matcher(route).results()
.map(match -> match.group(1))
.distinct()
.toList();
}

private static String normalizeRoute(String value) {
var normalized = value.trim();
if (!normalized.startsWith("/")) {
normalized = "/" + normalized;
}
while (normalized.length() > 1 && normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized;
}
}
12 changes: 12 additions & 0 deletions cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/


import com.condation.cms.api.Constants;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -90,6 +91,17 @@ ContentQuery<T> within(

ContentQuery<T> expression(final String expressions);

/**
* Restricts the query to items whose title matches the input.
*
* Implementations with a full-text title index should override this method.
* The default keeps existing query implementations source compatible and uses
* their regular title-field matching.
*/
default ContentQuery<T> searchByTitle(final String input) {
return where(Constants.MetaFields.TITLE, input);
}

public static interface Sort<T> {
public ContentQuery<T> asc();

Expand Down
39 changes: 39 additions & 0 deletions cms-api/src/main/java/com/condation/cms/api/db/CursorPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.condation.cms.api.db;

/*-
* #%L
* CMS Api
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* 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 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 <http://www.gnu.org/licenses/>.
* #L%
*/

import java.util.List;

/**
* One forward-only cursor page. The cursor is opaque and expires when the
* underlying index changes.
*/
public record CursorPage<T>(List<T> items, String nextCursor) {

public CursorPage {
items = List.copyOf(items);
}

public boolean hasNext() {
return nextCursor != null && !nextCursor.isBlank();
}
}
3 changes: 3 additions & 0 deletions cms-api/src/main/java/com/condation/cms/api/db/DB.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

import com.condation.cms.api.db.taxonomy.Taxonomies;
import com.condation.cms.api.db.collection.Collections;
import com.condation.cms.api.db.cms.ReadOnlyFileSystem;


Expand All @@ -40,6 +41,8 @@ public interface DB extends AutoCloseable{
public ReadOnlyFileSystem getReadOnlyFileSystem();

public Content getContent();

public Collections getCollections();

public Taxonomies getTaxonomies();
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public interface DBFileSystem {

ReadOnlyFile contentBase();

ReadOnlyFile collectionsBase();

ReadOnlyFile assetBase();

/**
Expand Down
Loading
Loading