This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-doxia-sitetools.git
The following commit(s) were added to refs/heads/master by this push:
new ffe26f9 Expose SCM last modification date and author from Doxia
source in (#627)
ffe26f9 is described below
commit ffe26f9a21f32b0ad4daacf6b073443848c9fad1
Author: Konrad Windszus <[email protected]>
AuthorDate: Mon Mar 30 18:08:33 2026 +0200
Expose SCM last modification date and author from Doxia source in (#627)
The new Velocity property "scmModifiedDate" is receiving its value as
Date.
Allow to configure a date value source in the site descriptor. Default
behaviour is still "publishDate" as before.
This closes #279
---
doxia-site-model/src/main/mdo/site.mdo | 50 ++++++-
.../doxia/siterenderer/ContextCustomizer.java | 38 +++++
.../doxia/siterenderer/DefaultSiteRenderer.java | 30 +++-
.../doxia/siterenderer/SiteRenderingContext.java | 44 ++++++
.../src/main/resources/site-renderer.properties | 1 +
.../src/main/resources/site-renderer_de.properties | 1 +
doxia-site-renderer/src/site/apt/index.apt.vm | 10 +-
doxia-site-scm-context/pom.xml | 142 ++++++++++++++++++
.../ScmAttributesContextCustomizer.java | 165 +++++++++++++++++++++
.../ScmAttributesContextCustomizerIT.java | 82 ++++++++++
.../ScmAttributesContextCustomizerTest.java | 135 +++++++++++++++++
.../site-last-modified/markdown/lastmodified.md.vm | 21 +++
.../src/test/resources/site-last-modified/site.xml | 36 +++++
pom.xml | 6 +
14 files changed, 755 insertions(+), 6 deletions(-)
diff --git a/doxia-site-model/src/main/mdo/site.mdo
b/doxia-site-model/src/main/mdo/site.mdo
index e9fe62b..ce34f69 100644
--- a/doxia-site-model/src/main/mdo/site.mdo
+++ b/doxia-site-model/src/main/mdo/site.mdo
@@ -336,7 +336,7 @@ under the License.
<class java.clone="deep">
<name>PublishDate</name>
- <description>Modify display properties for date published.</description>
+ <description>Modify display properties for the date in the
header/footer. By default is the publish date.</description>
<version>1.0.0+</version>
<fields>
<field xml.attribute="true">
@@ -368,6 +368,54 @@ under the License.
<identifier>true</identifier>
<defaultValue>Etc/UTC</defaultValue>
</field>
+ <field xml.attribute="true">
+ <name>value</name>
+ <description>
+ <![CDATA[
+ The Velocity context key to use as source for the date. Usually
one of "publishDate" or "scmLastModified". Must have a value of type
<code>java.util.Date</code>.
+ ]]>
+ </description>
+ <version>2.1.0+</version>
+ <type>String</type>
+ <identifier>true</identifier>
+ <defaultValue>publishDate</defaultValue>
+ </field>
+ <field xml.attribute="true">
+ <name>prefix</name>
+ <description>
+ <![CDATA[
+ The prefix to emit in front of the date. When starting with
<code>template.</code> it is assumed to reference a key from the
<code>site-renderer.properties</code>, otherwise it is a literal value.
+ ]]>
+ </description>
+ <version>2.1.0+</version>
+ <type>String</type>
+ <identifier>true</identifier>
+ <defaultValue>template.lastpublished</defaultValue>
+ </field>
+ <field xml.attribute="true">
+ <name>fallbackValue</name>
+ <description>
+ <![CDATA[
+ The Velocity context key to use as source for the date. Used as
fallback when the context given in "value" is not available. Must have a value
of type <code>java.util.Date</code>. This is useful when the context given in
"value" is only available for Doxia source documents (but not for reports).
+ ]]>
+ </description>
+ <version>2.1.0+</version>
+ <type>String</type>
+ <identifier>true</identifier>
+ <defaultValue>date</defaultValue>
+ </field>
+ <field xml.attribute="true">
+ <name>fallbackPrefix</name>
+ <description>
+ <![CDATA[
+ The prefix to emit in front of the fallback date. When
starting with <code>template.</code> it is assumed to reference a key from the
<code>site-renderer.properties</code>, otherwise it is a literal value.
+ ]]>
+ </description>
+ <version>2.1.0+</version>
+ <type>String</type>
+ <identifier>true</identifier>
+ <defaultValue>template.lastpublished</defaultValue>
+ </field>
</fields>
</class>
diff --git
a/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/ContextCustomizer.java
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/ContextCustomizer.java
new file mode 100644
index 0000000..bca2764
--- /dev/null
+++
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/ContextCustomizer.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.maven.doxia.siterenderer;
+
+import org.apache.velocity.context.Context;
+
+/**
+ * A callback interface to customize the Velocity {@link Context} before
rendering a document.
+ **/
+@FunctionalInterface
+public interface ContextCustomizer {
+
+ /**
+ * Customize the Velocity context before rendering a document.
+ *
+ * @param context the Velocity context to customize.
+ * @param docRenderingContext the document rendering context for the
document being rendered.
+ * @param siteRenderingContext the site rendering context for the site
being rendered.
+ */
+ void customizeContext(
+ Context context, DocumentRenderingContext docRenderingContext,
SiteRenderingContext siteRenderingContext);
+}
diff --git
a/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/DefaultSiteRenderer.java
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/DefaultSiteRenderer.java
index 861cfea..bc6be80 100644
---
a/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/DefaultSiteRenderer.java
+++
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/DefaultSiteRenderer.java
@@ -143,6 +143,9 @@ public class DefaultSiteRenderer implements Renderer {
@Inject
private PlexusContainer plexus;
+ @Inject
+ private Map<String, ContextCustomizer> contextCustomizers;
+
private static final String SKIN_TEMPLATE_LOCATION =
"META-INF/maven/site.vm";
private static final String TOOLS_LOCATION =
"META-INF/maven/site-tools.xml";
@@ -589,6 +592,18 @@ public class DefaultSiteRenderer implements Renderer {
context.put("alignedFilePath", alignedFilePath);
// TODO Deprecated -- will be removed!
context.put("alignedFileName", alignedFilePath);
+
+ for (Map.Entry<String, ContextCustomizer> entry :
contextCustomizers.entrySet()) {
+ try {
+ LOGGER.debug("Applying Velocity context customizer '" +
entry.getKey() + "'");
+ entry.getValue().customizeContext(context,
docRenderingContext, siteRenderingContext);
+ } catch (Exception e) {
+ LOGGER.warn(
+ "Velocity context customizer '" + entry.getKey()
+ + "' threw an exception and will be
ignored",
+ e);
+ }
+ }
}
context.put("site", siteRenderingContext.getSiteModel());
// TODO Deprecated -- will be removed!
@@ -640,7 +655,13 @@ public class DefaultSiteRenderer implements Renderer {
// then add data objects from rendered document
// Add infos from document
- context.put("authors", content.getAuthors());
+ Collection<String> authors = content.getAuthors();
+ if (authors != null && !authors.isEmpty()) {
+ context.put("authors", authors);
+ } else {
+ // use scmModifiedDate (if available) as fallback
+ context.put("authors", context.get("scmModifiedAuthor"));
+ }
String shortTitle = content.getTitle();
context.put("shortTitle", shortTitle);
@@ -672,7 +693,12 @@ public class DefaultSiteRenderer implements Renderer {
context.put("bodyContent", content.getBody());
// document date (got from Doxia Sink date() API)
- context.put("documentDate", content.getDate());
+ if (content.getDate() != null) {
+ context.put("documentDate", content.getDate());
+ } else {
+ // use scmModifiedDate (if available) as fallback
+ context.put("documentDate", context.get("scmModifiedDate"));
+ }
// document rendering context, to get eventual inputPath
context.put("docRenderingContext", content.getRenderingContext());
diff --git
a/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/SiteRenderingContext.java
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/SiteRenderingContext.java
index 6b461f9..08884a4 100644
---
a/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/SiteRenderingContext.java
+++
b/doxia-site-renderer/src/main/java/org/apache/maven/doxia/siterenderer/SiteRenderingContext.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
@@ -145,6 +146,8 @@ public class SiteRenderingContext {
private ParserConfigurator parserConfigurator;
+ private final Map<String, Object> attributes = new HashMap<>();
+
/**
* If input documents should be validated before parsing.
* By default no validation is performed.
@@ -488,4 +491,45 @@ public class SiteRenderingContext {
public void setParserConfigurator(ParserConfigurator parserConfigurator) {
this.parserConfigurator = parserConfigurator;
}
+
+ /**
+ * Gets the map of attributes that can be used to cache items per site
rendering context.
+ * This is a free-form map that can be used for example
+ * from the {@link ContextCustomizer} to cache items that can be reused
among different documents of the same site.
+ *
+ * @return a map of attributes that can be used by renderers and templates
to customize the output, the returned map is immutable, use {@link
#addAttribute(String, Object)} to add attributes to the context
+ * @see #putAttribute(String, Object)
+ * @see #removeAttribute(String)
+ * @since 2.1.0
+ */
+ public Map<String, Object> getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+
+ /**
+ * Puts an attribute in the context attributes map, this can be used to
cache items per site rendering context.
+ * It overrides any existing value for the same key and returns the
previous value associated with the key, or {@code null}
+ * if there was no mapping for the key or if the map previously associated
{@code null} with the key.
+ *
+ * @return the previous value associated with the key, or {@code null}
+ * @see #getAttributes()
+ * @see #removeAttribute(String)
+ * @since 2.1.0
+ */
+ public Object putAttribute(String key, Object value) {
+ return this.attributes.put(key, value);
+ }
+
+ /**
+ * Removes the mapping for a key from this attributes map if it is present.
+ *
+ * @param key key whose mapping is to be removed from the map
+ * @return the previous value associated with key, or {@code null} if
there was no mapping for key or if the map previously associated {@code null}
with the key.
+ * @see #getAttributes()
+ * @see #putAttribute(String, Object)
+ * @since 2.1.0
+ */
+ public Object removeAttribute(String key) {
+ return this.attributes.remove(key);
+ }
}
diff --git a/doxia-site-renderer/src/main/resources/site-renderer.properties
b/doxia-site-renderer/src/main/resources/site-renderer.properties
index 793e4ea..8a1d8bc 100644
--- a/doxia-site-renderer/src/main/resources/site-renderer.properties
+++ b/doxia-site-renderer/src/main/resources/site-renderer.properties
@@ -16,6 +16,7 @@
# under the License.
template.lastpublished=Last Published
+template.lastmodified=Last Modified
template.version=Version
template.builtby=Built by
template.externallinks=External Links
diff --git a/doxia-site-renderer/src/main/resources/site-renderer_de.properties
b/doxia-site-renderer/src/main/resources/site-renderer_de.properties
index 1f8d8ec..6fd929b 100644
--- a/doxia-site-renderer/src/main/resources/site-renderer_de.properties
+++ b/doxia-site-renderer/src/main/resources/site-renderer_de.properties
@@ -16,6 +16,7 @@
# under the License.
template.lastpublished=Zuletzt ver\u00F6ffentlicht
+template.lastmodified=Zuletzt ge\u00F6ndert
template.builtby=Erstellt von
template.externallinks=Externe Links
template.edit=Bearbeiten
diff --git a/doxia-site-renderer/src/site/apt/index.apt.vm
b/doxia-site-renderer/src/site/apt/index.apt.vm
index e97abd6..2e301ec 100644
--- a/doxia-site-renderer/src/site/apt/index.apt.vm
+++ b/doxia-site-renderer/src/site/apt/index.apt.vm
@@ -75,6 +75,10 @@ Doxia Sitetools - Site Renderer
*---------------------------------+----------------------+-------------------------------+
| <<<relativePath>>> | <<<String>>> | The path to the
site root from the document being rendered. |
*---------------------------------+----------------------+-------------------------------+
+| <<<scmModifiedDate>>> | <<<Date>>> | The last modified
date as retrieved through {{{https://maven.apache.org/scm/}Maven SCM}} from the
underlying Doxia source. Only available if artifact
{{{../doxia-site-scm-context/}<<<doxia-site-scm-context>>>}} is on the
classpath. |
+*---------------------------------+----------------------+-------------------------------+
+| <<<scmModifiedAuthor>>> | <<<String>>> | The author of the
last commit on the file as retrieved through
{{{https://maven.apache.org/scm/}Maven SCM}} from the underlying Doxia source.
Only available if artifact
{{{../doxia-site-scm-context/}<<<doxia-site-scm-context>>>}} is on the
classpath. |
+*---------------------------------+----------------------+-------------------------------+
| <<<site>>> |
{{{../doxia-site-model/apidocs/org/apache/maven/doxia/site/SiteModel.html}<<<SiteModel>>>}}
| This is a model that represents the data in your
{{{../doxia-site-model/site.html}<<<site.xml>>>}}. |
*---------------------------------+----------------------+-------------------------------+
| <<<supportedLocales>>> | <<<List\<Locale\>>>> | The list of locales
that the site will contain. |
@@ -108,7 +112,7 @@ Doxia Sitetools - Site Renderer
*------------------+---------------------------------------------------------------+-------------------------------+
| <<<convert>>> | {{{$generic${esc.hash}deprecated-tools}ConversionTool}}
| {{{$generic${esc.hash}deprecated-tools}<<Deprecated>>}}: use NumberTool
for numbers formatting/parsing, DateTool for date/time formatting/parsing, or
CollectionTool for toStrings(). For converting String values to richer object
Types.
*------------------+---------------------------------------------------------------+-------------------------------+
-| <<<date>>> |
{{{$generic${esc.hash}ComparisonDateTool}ComparisonDateTool}} | For
manipulating, formatting, and comparing dates.
+| <<<date>>> |
{{{$generic${esc.hash}ComparisonDateTool}ComparisonDateTool}} | For
manipulating, formatting, and comparing dates. The formatting pattern and
timezone are taken from the site descriptors element "publishDate" "format and
"timezone" attributes, respectively.
*------------------+---------------------------------------------------------------+-------------------------------+
| <<<display>>> | {{{$generic${esc.hash}DisplayTool}DisplayTool}}
| For controlling display of references (e.g., truncating values, "pretty
printing" lists, and displaying alternates when a reference is null).
*------------------+---------------------------------------------------------------+-------------------------------+
@@ -166,11 +170,11 @@ Doxia Sitetools - Site Renderer
*---------------------------------+----------------------+-------------------------------+
|| Variable || Type || Description
||
*---------------------------------+----------------------+-------------------------------+
-| <<<authors>>> | <<<List\<String\>>>> | A list of authors
from the source document. |
+| <<<authors>>> | <<<List\<String\>>>> | A list of authors
from the source document. If not set is equal to <<<scmModifiedAuthor>>> (only
available if artifact
{{{../doxia-site-scm-context/}<<<doxia-site-scm-context>>>}} is on the
classpath). | |
*---------------------------------+----------------------+-------------------------------+
| <<<bodyContent>>> | <<<String>>> | HTML body content
of the Doxia generated output. |
*---------------------------------+----------------------+-------------------------------+
-| <<<documentDate>>> | <<<String>>> | The date specified
in the source document: semantics has to be chosen by document writer (document
creation date, or document last modification date, or ...), and format is not
enforced. |
+| <<<documentDate>>> | <<<String>>> | The date specified
in the source document: semantics has to be chosen by document writer (document
creation date, or document last modification date, or ...), and format is not
enforced. If not set is equal to <<<scmModifiedDate>>> (only available if
artifact {{{../doxia-site-scm-context/}<<<doxia-site-scm-context>>>}} is on the
classpath). |
*---------------------------------+----------------------+-------------------------------+
| <<<headContent>>> | <<<String>>> | HTML head content
of the Doxia generated output. |
*---------------------------------+----------------------+-------------------------------+
diff --git a/doxia-site-scm-context/pom.xml b/doxia-site-scm-context/pom.xml
new file mode 100644
index 0000000..045feaa
--- /dev/null
+++ b/doxia-site-scm-context/pom.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.maven.doxia</groupId>
+ <artifactId>doxia-sitetools</artifactId>
+ <version>2.1.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>doxia-site-scm-context</artifactId>
+
+ <name>Doxia Sitetools :: SCM Context</name>
+ <description>Extends the document's Velocity context by SCM
metadata.</description>
+
+ <properties>
+ <scmVersion>2.2.1</scmVersion>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-artifact</artifactId>
+ <version>${mavenVersion}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.maven.doxia</groupId>
+ <artifactId>doxia-site-renderer</artifactId>
+ </dependency>
+
+ <!-- scm dependencies -->
+ <dependency>
+ <groupId>org.apache.maven.scm</groupId>
+ <artifactId>maven-scm-api</artifactId>
+ <version>${scmVersion}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.scm</groupId>
+ <artifactId>maven-scm-providers-standard</artifactId>
+ <version>${scmVersion}</version>
+ <type>pom</type>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.scm</groupId>
+ <artifactId>maven-scm-manager-plexus</artifactId>
+ <version>${scmVersion}</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <!-- test -->
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-testing</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>4.11.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>4.11.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>run-its</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git
a/doxia-site-scm-context/src/main/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizer.java
b/doxia-site-scm-context/src/main/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizer.java
new file mode 100644
index 0000000..4238a86
--- /dev/null
+++
b/doxia-site-scm-context/src/main/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizer.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.maven.doxia.scm.siterenderer;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Optional;
+
+import org.apache.maven.doxia.siterenderer.ContextCustomizer;
+import org.apache.maven.doxia.siterenderer.DocumentRenderingContext;
+import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
+import org.apache.maven.scm.ScmException;
+import org.apache.maven.scm.ScmFileSet;
+import org.apache.maven.scm.command.info.InfoItem;
+import org.apache.maven.scm.command.info.InfoScmResult;
+import org.apache.maven.scm.manager.ScmManager;
+import org.apache.maven.scm.repository.ScmRepository;
+import org.apache.velocity.context.Context;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link ContextCustomizer} that adds SCM attributes to the Velocity
context for use in templates.
+ * It looks for an SCM repository in the directory of the site being rendered,
and if found, retrieves SCM info for the file being rendered and adds it to the
Velocity context.
+ *
+ * The SCM repository is lazily retrieved and cached in the
SiteRenderingContext attributes for subsequent retrievals, so that it is only
looked up once per site rendering.
+ *
+ * The SCM info is retrieved for each document being rendered, but only if an
SCM repository was found for the site.
+ *
+ * The following attributes are added to the Velocity context:
+ * <ul>
+ * <li>{@value #ATTRIBUTE_NAME_SCM_MODIFIED_DATE}: the last modification date
of the file being rendered according to SCM, as a {@link java.util.Date} (if
available)</li>
+ * <li>{@value #ATTRIBUTE_NAME_SCM_MODIFIED_AUTHOR}: the author of the last
modification of the file being rendered according to SCM, as a {@link String}
(if available)</li>
+ * <ul>
+ *
+ * @since 2.1.0
+ */
+@Singleton
+@Named("scmAttributes")
+public class ScmAttributesContextCustomizer implements ContextCustomizer {
+
+ private static final String ATTRIBUTE_NAME_SCM_MODIFIED_AUTHOR =
"scmModifiedAuthor";
+
+ private static final String ATTRIBUTE_NAME_SCM_MODIFIED_DATE =
"scmModifiedDate";
+
+ private static final String KEY_SCM_REPOSITORY =
"org.apache.maven.doxia.scm.siterenderer.scmRepository";
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ScmAttributesContextCustomizer.class);
+
+ private final ScmManager scmManager;
+
+ @Inject
+ ScmAttributesContextCustomizer(ScmManager scmManager) {
+ this.scmManager = scmManager;
+ }
+
+ /**
+ * Lazily retrieves the SCM repository for the site being rendered,
caching it in the SiteRenderingContext attributes for subsequent retrievals.
+ * @param siteRenderingContext
+ * @return an Optional containing the SCM repository if found, or an empty
Optional if not found or if an error occurs while trying to find it.
+ */
+ private Optional<ScmRepository> getScmRepository(SiteRenderingContext
siteRenderingContext) {
+ if
(siteRenderingContext.getAttributes().containsKey(KEY_SCM_REPOSITORY)) {
+ return Optional.ofNullable(
+ (ScmRepository)
siteRenderingContext.getAttributes().get(KEY_SCM_REPOSITORY));
+ } else {
+ Optional<ScmRepository> scmRepository =
+ getScmRepository(scmManager,
siteRenderingContext.getRootDirectory());
+ siteRenderingContext.putAttribute(KEY_SCM_REPOSITORY,
scmRepository.orElse(null));
+ return scmRepository;
+ }
+ }
+
+ static Optional<ScmRepository> getScmRepository(ScmManager scmManager,
File directory) {
+ Optional<ScmRepository> scmRepository =
scmManager.makeProviderScmRepository(directory);
+ if (scmRepository.isPresent()) {
+ LOGGER.debug("Found SCM repository for directory \"{}\"",
directory);
+ } else {
+ LOGGER.debug("No SCM repository found for directory {}",
directory);
+ File parentDirectory = directory.getParentFile();
+ if (parentDirectory != null) {
+ return getScmRepository(scmManager, parentDirectory);
+ }
+ }
+ return scmRepository;
+ }
+
+ static InfoItem getScmInfo(ScmManager scmManager, ScmRepository
scmRepository, File file) {
+ try {
+ ScmFileSet fileSet = new ScmFileSet(file.getParentFile(),
Collections.singletonList(file));
+ InfoScmResult infos = scmManager
+ .getProviderByRepository(scmRepository)
+ .info(scmRepository.getProviderRepository(), fileSet,
null);
+ if (infos != null && infos.isSuccess() &&
!infos.getInfoItems().isEmpty()) {
+ return infos.getInfoItems().get(0);
+ } else {
+ LOGGER.warn("Failed to get SCM info for file \"{}\": {}",
file, infos);
+ }
+ } catch (ScmException e) {
+ LOGGER.warn("Failed to get SCM info for file \"{}\"", file, e);
+ }
+ return null;
+ }
+
+ @Override
+ public void customizeContext(
+ Context context, DocumentRenderingContext docRenderingContext,
SiteRenderingContext siteRenderingContext) {
+ File inputFile = new File(docRenderingContext.getBasedir(),
docRenderingContext.getInputPath());
+ if (!inputFile.exists()) {
+ LOGGER.debug("Input file \"{}\" does not exist, cannot retrieve
SCM info", inputFile);
+ return;
+ }
+ Optional<ScmRepository> scmRepository =
getScmRepository(siteRenderingContext);
+
+ final InfoItem scmInfo;
+ if (scmRepository.isPresent()) {
+ scmInfo = getScmInfo(scmManager, scmRepository.get(), inputFile);
+ } else {
+ scmInfo = null;
+ }
+
+ if (scmInfo != null) {
+ if (scmInfo.getLastChangedDateTime() != null) {
+ // Velocity can only deal with Date/Calendar
+ Date scmModifiedDate =
+
Date.from(scmInfo.getLastChangedDateTime().toInstant());
+ context.put(ATTRIBUTE_NAME_SCM_MODIFIED_DATE, scmModifiedDate);
+ } else {
+ LOGGER.warn(
+ "SCM info for file \"{}\" does not contain last
modification date, maybe not yet committed or not existing?",
+ inputFile);
+ }
+ if (scmInfo.getLastChangedAuthor() != null) {
+ context.put(ATTRIBUTE_NAME_SCM_MODIFIED_AUTHOR,
scmInfo.getLastChangedAuthor());
+ } else {
+ LOGGER.warn(
+ "SCM info for file \"{}\" does not contain last
author, maybe not yet committed or not existing?",
+ inputFile);
+ }
+ } else {
+ LOGGER.debug("No SCM info available for file \"{}\"", inputFile);
+ }
+ }
+}
diff --git
a/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerIT.java
b/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerIT.java
new file mode 100644
index 0000000..bd051d4
--- /dev/null
+++
b/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerIT.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.maven.doxia.scm.siterenderer;
+
+import javax.inject.Inject;
+
+import java.io.File;
+import java.util.Optional;
+
+import org.apache.maven.doxia.siterenderer.ContextCustomizer;
+import org.apache.maven.doxia.siterenderer.DocumentRenderingContext;
+import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
+import org.apache.maven.scm.command.info.InfoItem;
+import org.apache.maven.scm.manager.ScmManager;
+import org.apache.maven.scm.repository.ScmRepository;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.context.Context;
+import org.codehaus.plexus.testing.PlexusTest;
+import org.junit.jupiter.api.Test;
+
+import static org.codehaus.plexus.testing.PlexusExtension.getTestFile;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+@PlexusTest
+class ScmAttributesContextCustomizerIT {
+
+ @Inject
+ ScmManager scmManager;
+
+ @Test
+ void scmAttributes() throws Exception {
+ File siteDirectory =
getTestFile("src/test/resources/site-last-modified");
+ File doxiaSource = new File(siteDirectory,
"markdown/lastmodified.md.vm");
+ assertTrue(doxiaSource.exists(), "Test source file does not exist: " +
doxiaSource.getAbsolutePath());
+ assumeTrue(isScmInfoAvailable(doxiaSource), "SCM info is not
available, skipping test");
+
+ ContextCustomizer contextCustomizer = new
ScmAttributesContextCustomizer(scmManager);
+ Context context = new VelocityContext();
+ DocumentRenderingContext docContext =
+ new DocumentRenderingContext(doxiaSource.getParentFile(),
doxiaSource.getName(), null);
+ SiteRenderingContext siteContext = new SiteRenderingContext();
+ siteContext.setRootDirectory(siteDirectory);
+ contextCustomizer.customizeContext(context, docContext, siteContext);
+ assertTrue(context.containsKey("scmModifiedDate"));
+ assertTrue(context.containsKey("scmModifiedAuthor"));
+ // the actual values depends a bit on the actual commit history, so we
just check that they are not empty
+ }
+
+ boolean isScmInfoAvailable(File file) {
+ try {
+ Optional<ScmRepository> repo =
+
ScmAttributesContextCustomizer.getScmRepository(scmManager,
file.getParentFile());
+ if (!repo.isPresent()) {
+ return false;
+ }
+ InfoItem infoItem =
ScmAttributesContextCustomizer.getScmInfo(scmManager, repo.get(), file);
+ if (infoItem != null && infoItem.getLastChangedDateTime() != null)
{
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git
a/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerTest.java
b/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerTest.java
new file mode 100644
index 0000000..8c86646
--- /dev/null
+++
b/doxia-site-scm-context/src/test/java/org/apache/maven/doxia/scm/siterenderer/ScmAttributesContextCustomizerTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.maven.doxia.scm.siterenderer;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Optional;
+
+import org.apache.maven.doxia.siterenderer.ContextCustomizer;
+import org.apache.maven.doxia.siterenderer.DocumentRenderingContext;
+import org.apache.maven.doxia.siterenderer.SiteRenderingContext;
+import org.apache.maven.scm.command.info.InfoItem;
+import org.apache.maven.scm.command.info.InfoScmResult;
+import org.apache.maven.scm.manager.ScmManager;
+import org.apache.maven.scm.provider.ScmProvider;
+import org.apache.maven.scm.provider.ScmProviderRepository;
+import org.apache.maven.scm.repository.ScmRepository;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.context.Context;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(MockitoExtension.class)
+class ScmAttributesContextCustomizerTest {
+
+ @Mock
+ ScmManager scmManager;
+
+ @Mock
+ ScmRepository scmRepository;
+
+ @Mock
+ ScmProvider scmProvider;
+
+ @Mock
+ ScmProviderRepository scmProviderRepository;
+
+ private ContextCustomizer contextCustomizer;
+
+ private Context context;
+
+ private DocumentRenderingContext docContext;
+
+ private SiteRenderingContext siteContext;
+
+ private File siteDirectory;
+
+ @BeforeEach
+ void setup() throws IOException {
+ siteDirectory = new
File("src/test/resources/site-last-modified").getCanonicalFile();
+ contextCustomizer = new ScmAttributesContextCustomizer(scmManager);
+ context = new VelocityContext();
+ docContext = new DocumentRenderingContext(siteDirectory,
"markdown/lastmodified.md.vm", null);
+ siteContext = new SiteRenderingContext();
+ siteContext.setRootDirectory(siteDirectory);
+ }
+
+ @Test
+ void lastModifiedDate() throws Exception {
+
Mockito.when(scmManager.makeProviderScmRepository(siteDirectory)).thenReturn(Optional.of(scmRepository));
+
Mockito.when(scmManager.getProviderByRepository(scmRepository)).thenReturn(scmProvider);
+
Mockito.when(scmRepository.getProviderRepository()).thenReturn(scmProviderRepository);
+
+ InfoItem infoItem = new InfoItem();
+ OffsetDateTime modifiedDate = OffsetDateTime.of(2024, 6, 1, 12, 0, 0,
0, ZoneOffset.UTC);
+ infoItem.setLastChangedDateTime(modifiedDate);
+
+ // equals not properly for any of the info(...) parameters, so use
Mockito.any() for all parameters
+ Mockito.when(scmProvider.info(Mockito.any(), Mockito.any(),
Mockito.any()))
+ .thenReturn(new InfoScmResult("",
Collections.singletonList(infoItem)));
+
+ contextCustomizer.customizeContext(context, docContext, siteContext);
+ assertTrue(context.containsKey("scmModifiedDate"));
+ assertEquals(Date.from(modifiedDate.toInstant()),
context.get("scmModifiedDate"));
+ }
+
+ @Test
+ void lastModifiedDateOutsideRepo() throws Exception {
+
Mockito.when(scmManager.makeProviderScmRepository(siteDirectory)).thenReturn(Optional.empty());
+ contextCustomizer.customizeContext(context, docContext, siteContext);
+ assertFalse(context.containsKey("scmModifiedDate"));
+ }
+
+ @Test
+ void lastModifiedDateUnknown() throws Exception {
+
Mockito.when(scmManager.makeProviderScmRepository(siteDirectory)).thenReturn(Optional.of(scmRepository));
+
Mockito.when(scmManager.getProviderByRepository(scmRepository)).thenReturn(scmProvider);
+
Mockito.when(scmRepository.getProviderRepository()).thenReturn(scmProviderRepository);
+
+ InfoItem infoItem = new InfoItem();
+
+ // equals not properly for any of the info(...) parameters, so use
Mockito.any() for all parameters
+ Mockito.when(scmProvider.info(Mockito.any(), Mockito.any(),
Mockito.any()))
+ .thenReturn(new InfoScmResult("",
Collections.singletonList(infoItem)));
+ contextCustomizer.customizeContext(context, docContext, siteContext);
+ assertFalse(context.containsKey("scmModifiedDate"));
+ }
+
+ @Test
+ void lastModifiedDateNonExisting() throws Exception {
+ docContext = new DocumentRenderingContext(siteDirectory,
"markdown/non-existing.md", null);
+ Mockito.verifyNoInteractions(scmManager);
+
+ contextCustomizer.customizeContext(context, docContext, siteContext);
+ assertFalse(context.containsKey("scmModifiedDate"));
+ }
+}
diff --git
a/doxia-site-scm-context/src/test/resources/site-last-modified/markdown/lastmodified.md.vm
b/doxia-site-scm-context/src/test/resources/site-last-modified/markdown/lastmodified.md.vm
new file mode 100644
index 0000000..5f8fe8a
--- /dev/null
+++
b/doxia-site-scm-context/src/test/resources/site-last-modified/markdown/lastmodified.md.vm
@@ -0,0 +1,21 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+<!-- must be formatted according to site descriptor "publishDate format" -->
+Last modified: $date.format($scmModifiedDate)
\ No newline at end of file
diff --git
a/doxia-site-scm-context/src/test/resources/site-last-modified/site.xml
b/doxia-site-scm-context/src/test/resources/site-last-modified/site.xml
new file mode 100644
index 0000000..66fe6e9
--- /dev/null
+++ b/doxia-site-scm-context/src/test/resources/site-last-modified/site.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<site xmlns="http://maven.apache.org/SITE/2.1.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SITE/2.1.0
../../../../../doxia-site-model/target/generated-site/resources/xsd/site-2.1.0.xsd"
+ name="Plexus">
+ <bannerLeft name="Plexus" href="http://plexus.codehaus.org">
+ <image src="http://plexus.codehaus.org/images/plexus-logo.png" />
+ </bannerLeft>
+ <bannerRight href="http://www.codehaus.org">
+ <image src="http://media.codehaus.org/images/unity-codehaus-logo.png" />
+ </bannerRight>
+ <skin>
+ <groupId>org.apache.maven.skins</groupId>
+ <artifactId>maven-fluido-skin</artifactId>
+ </skin>
+ <publishDate position="none" format="yyyy:MM:dd" />
+ <modificationDate position="left" />
+</site>
diff --git a/pom.xml b/pom.xml
index 5069eb3..a18b7a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,7 @@ under the License.
<module>doxia-skin-model</module>
<module>doxia-integration-tools</module>
<module>doxia-site-renderer</module>
+ <module>doxia-site-scm-context</module>
</modules>
<scm>
@@ -125,6 +126,11 @@ under the License.
<artifactId>doxia-skin-model</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.maven.doxia</groupId>
+ <artifactId>doxia-site-renderer</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<!-- Commons -->
<dependency>
<groupId>commons-io</groupId>