1. Project Clover database mer. févr. 4 2026 12:48:28 CET
  2. Package org.devacfr.maven.skins.reflow

File SkinConfigTool.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart6.png
71% of files have more coverage

Code metrics

152
309
49
1
1 062
635
141
0,46
6,31
49
2,88

Classes

Class Line # Actions
SkinConfigTool 96 309 0% 141 217
0.574509857,5%
 

Contributing tests

This file is covered by 26 tests. .

Source view

1    /*
2    * Copyright 2012-2025 Christophe Friederich
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10    * Unless required by applicable law or agreed to in writing, software
11    * distributed under the License is distributed on an "AS IS" BASIS,
12    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    * See the License for the specific language governing permissions and
14    * limitations under the License.
15    */
16    package org.devacfr.maven.skins.reflow;
17   
18    import static java.util.Objects.requireNonNull;
19   
20    import com.google.common.base.Strings;
21    import java.text.ParseException;
22    import java.text.SimpleDateFormat;
23    import java.util.Date;
24    import java.util.List;
25    import java.util.Optional;
26    import javax.annotation.Nonnull;
27    import javax.annotation.Nullable;
28    import org.apache.commons.lang3.StringUtils;
29    import org.apache.maven.doxia.site.PublishDate;
30    import org.apache.maven.doxia.site.SiteModel;
31    import org.apache.maven.project.MavenProject;
32    import org.apache.velocity.tools.ToolContext;
33    import org.apache.velocity.tools.config.DefaultKey;
34    import org.apache.velocity.tools.generic.DateTool;
35    import org.apache.velocity.tools.generic.RenderTool;
36    import org.apache.velocity.tools.generic.ResourceTool;
37    import org.apache.velocity.tools.generic.SafeConfig;
38    import org.apache.velocity.tools.generic.ValueParser;
39    import org.codehaus.plexus.util.PathTool;
40    import org.codehaus.plexus.util.xml.Xpp3Dom;
41    import org.devacfr.maven.skins.reflow.context.Context;
42    import org.devacfr.maven.skins.reflow.context.PositionType;
43    import org.devacfr.maven.skins.reflow.snippet.Component;
44    import org.devacfr.maven.skins.reflow.snippet.SnippetComponent;
45    import org.devacfr.maven.skins.reflow.snippet.SnippetContext;
46    import org.devacfr.maven.skins.reflow.snippet.SnippetParser;
47    import org.jsoup.nodes.Document;
48    import org.slf4j.Logger;
49    import org.slf4j.LoggerFactory;
50   
51    /**
52    * An Apache Velocity tool that simplifies retrieval of custom configuration values for a Maven Site.
53    * <p>
54    * The tool is configured to access Maven site configuration of a skin inside {@code <custom>} element of site
55    * descriptor. It supports global properties (defined at skin level) and per-page properties (defined in
56    * {@code <page><mypage>} element). The per-page properties override the global ones.
57    * </p>
58    * <p>
59    * A sample configuration would be like that:
60    * </p>
61    *
62    * <pre>
63    * {@code
64    * <custom>
65    * <reflowSkin>
66    * <prop1>value1</prop1>
67    * <prop2>
68    * <prop21>value2</prop21>
69    * </prop2>
70    * <pages>
71    * <mypage project="myproject">
72    * <prop1>override value1</prop1>
73    * </mypage>
74    * </pages>
75    * </reflowSkin>
76    * </custom>
77    * }
78    * </pre>
79    * <p>
80    * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return "override value1".
81    * Then {@code $config.prop2} would return "value2" - the global value.
82    * </p>
83    * <p>
84    * The tool allows querying the value easily, falling back from page to global configuration to {@code null}, if none is
85    * available. It also provides convenience accessors for common values.
86    * </p>
87    * <p>
88    * Note
89    * </p>
90    *
91    * @author Andrius Velykis
92    * @author Christophe Friederich
93    * @since 1.0
94    */
95    @DefaultKey("config")
 
96    public class SkinConfigTool extends SafeConfig implements ISkinConfig {
97   
98    /** */
99    private static final Logger LOGGER = LoggerFactory.getLogger(SkinConfigTool.class);
100   
101    // ISO 8601 BASIC is used by build timestamp
102    private static final SimpleDateFormat ISO_8601BASIC_DATE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
103   
104    private static final String PROJECT_BUILD_OUTPUTTIMESTAMP = "project.build.outputTimestamp";
105   
106    /** */
107    public static final String DEFAULT_KEY = "config";
108   
109    /** By default use Reflow skin configuration tag. */
110    public static final String SKIN_KEY = "reflowSkin";
111   
112    /** */
113    private String key = DEFAULT_KEY;
114   
115    /** */
116    private String skinKey = SKIN_KEY;
117   
118    /** Create dummy nodes to avoid null checks. */
119    private Xpp3Dom globalProperties = new Xpp3Dom("");
120   
121    /** */
122    private Xpp3Dom pageProperties = new Xpp3Dom("");
123   
124    /** */
125    private String namespace = "";
126   
127    /** */
128    private String projectId = null;
129   
130    /** */
131    private String fileId = null;
132   
133    /** */
134    private Context<?> context = null;
135   
136    /** */
137    private MavenProject project = null;
138   
139    /** */
140    private SiteModel siteModel;
141   
142    /** */
143    private ToolContext velocityContext;
144   
145    /**
146    * {@inheritDoc}
147    *
148    * @see SafeConfig#configure(ValueParser)
149    */
 
150  22 toggle @Override
151    protected void configure(final ValueParser values) {
152  22 final String altkey = values.getString("key");
153  22 if (altkey != null) {
154  0 setKey(altkey);
155    }
156   
157    // allow changing skin key in the configuration
158  22 final String altSkinKey = values.getString("skinKey");
159  22 if (altSkinKey != null) {
160  0 this.skinKey = altSkinKey;
161    }
162   
163    // retrieve the site model from Velocity context
164  22 final Object vc = values.get("velocityContext");
165   
166  22 if (!(vc instanceof ToolContext)) {
167  0 return;
168    }
169   
170  22 this.velocityContext = (ToolContext) vc;
171   
172  22 final Object projectObj = velocityContext.get("project");
173  22 if (projectObj instanceof MavenProject) {
174  22 this.project = (MavenProject) projectObj;
175  22 final String artifactId = project.getArtifactId();
176    // use artifactId "sluggified" as the projectId
177  22 projectId = HtmlTool.slug(artifactId);
178    }
179   
180    // calculate the page ID from the current file name
181  22 final String currentFileObj = getCurrentFileName();
182  22 fileId = slugFilename(currentFileObj);
183   
184  22 final Object siteModelObj = velocityContext.get("site");
185   
186  22 if (!(siteModelObj instanceof SiteModel)) {
187  0 return;
188    }
189   
190  22 this.siteModel = (SiteModel) siteModelObj;
191  22 final Object customObj = siteModel.getCustom();
192   
193  22 if (!(customObj instanceof Xpp3Dom)) {
194  0 return;
195    }
196   
197    // Now that we have the custom node, get the global properties
198    // under the skin tag
199  22 final Xpp3Dom customNode = (Xpp3Dom) customObj;
200  22 Xpp3Dom skinNode = customNode.getChild(skinKey);
201  22 final String namespaceKey = ":" + skinKey;
202   
203  22 if (skinNode == null) {
204    // try searching with any namespace
205  0 for (final Xpp3Dom child : customNode.getChildren()) {
206  0 if (child.getName().endsWith(namespaceKey)) {
207  0 skinNode = child;
208  0 break;
209    }
210    }
211    }
212   
213  22 if (skinNode != null) {
214  22 globalProperties = skinNode;
215   
216  22 if (skinNode.getName().endsWith(namespaceKey)) {
217    // extract the namespace (including the colon)
218  0 namespace = Strings
219    .emptyToNull(skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1));
220    }
221   
222    // for page properties, retrieve the file name and drop the `.html`
223    // extension - this will be used, i.e. `index` instead of `index.html`
224  22 final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(skinNode, "pages", namespace);
225  22 if (pagesNode != null) {
226   
227    // Get the page for the file
228  22 Xpp3Dom page = Xpp3Utils.getFirstChild(pagesNode, fileId, namespace);
229   
230    // Now check if the project artifact ID is set, and if so, if it matches the
231    // current project. This allows preventing accidental reuse of parent page
232    // configs in children modules
233  22 if (page != null && projectId != null) {
234  22 final String pageProject = page.getAttribute("project");
235  22 if (pageProject != null && !projectId.equals(pageProject)) {
236    // project ID indicated, and is different - do not use the config
237  0 page = null;
238    }
239    }
240   
241  22 if (page != null) {
242  22 pageProperties = page;
243    }
244    }
245   
246    // Config option <localResources>true</localResources> to force CDN-less
247    // Bootstrap & jQuery
248  22 this.velocityContext.put("localResources", is("localResources"));
249    // Use config option
250    // <absoluteResourceURL>http://mysite.com/</absoluteResourceURL>
251  22 this.velocityContext.put("resourcePath", getResourcePath());
252   
253  22 this.context = Context.buildContext(this);
254    }
255  22 if (LOGGER.isDebugEnabled()) {
256  22 LOGGER.debug("Current Filename: {}", currentFileObj);
257  22 LOGGER.debug("title: {}", velocityContext.get("title"));
258  22 LOGGER.debug("shortTitle: {}", velocityContext.get("shortTitle"));
259  22 LOGGER.debug("Project id: {}", projectId);
260  22 LOGGER.debug("File id: {}", fileId);
261  22 LOGGER.debug("Context: {}", this.context);
262  22 LOGGER.debug("Namespace: {}", this.namespace);
263  22 LOGGER.debug("---------------------------------------------------");
264    }
265    }
266   
267    /**
268    * {@inheritDoc}
269    */
 
270  0 toggle @Nonnull
271    @Override
272    public String renderSnippets(final String bodyContent) throws Exception {
273  0 final SnippetContext snippetContext = new SnippetParser().parse(this, bodyContent);
274  0 return snippetContext.html();
275    }
276   
277    /**
278    * {@inheritDoc}
279    */
 
280  2 toggle @Nonnull
281    @Override
282    public String renderSnippet(final String snippet) throws Exception {
283  2 final SnippetParser parser = new SnippetParser();
284  2 final Document doc = getHtmlTool().parse(snippet);
285  2 final SnippetContext context = parser.getSnippetContext();
286  2 final Component<?> component = context.create(doc.body().firstElementChild(), null);
287  2 context.setConfig(this);
288  2 if (component instanceof SnippetComponent<?>) {
289  2 ((SnippetComponent<?>) component).render(context);
290    }
291  2 return doc.body().html();
292    }
293   
294    /**
295    * Sets the key under which this tool has been configured.
296    *
297    * @param key
298    * the key of config
299    * @since 1.0
300    */
 
301  0 toggle protected void setKey(final String key) {
302  0 this.key = requireNonNull(key, "SkinConfigTool key cannot be null");
303    }
304   
305    /**
306    * @return Returns the key under which this tool has been configured. The default is `config`.
307    * @since 1.0
308    */
 
309  0 toggle public String getKey() {
310  0 return this.key;
311    }
312   
313    /**
314    * {@inheritDoc}
315    */
 
316  0 toggle @Override
317    @Nullable public <T> T getContextValue(@Nonnull final String key, @Nonnull final Class<T> type) {
318  0 requireNonNull(type);
319  0 if (String.class.isAssignableFrom(type)) {
320  0 return this.eval("$" + key, type);
321    } else {
322  0 throw new UnsupportedOperationException();
323    }
324    }
325   
326    /**
327    * {@inheritDoc}
328    */
 
329  0 toggle @Override
330    public void setContextValue(@Nonnull final String key, @Nullable final Object value) {
331  0 requireNonNull(key);
332  0 if (value instanceof String) {
333  0 this.eval("#set( $" + key + "= \"" + value.toString() + "\")", Void.class);
334    } else {
335  0 throw new UnsupportedOperationException();
336    }
337    }
338   
339    /**
340    * {@inheritDoc}
341    */
 
342  2 toggle @Override
343    @Nullable @SuppressWarnings("unchecked")
344    public <T> T getToolbox(@Nonnull final String toolName, @Nonnull final Class<T> toolType) {
345  2 requireNonNull(toolType);
346  2 return (T) this.velocityContext.getToolbox().get(requireNonNull(toolName));
347    }
348   
349    /**
350    * Gets the tool {@link HtmlTool}.
351    *
352    * @return Returns the tool {@link HtmlTool}.
353    * @since 2.1
354    */
 
355  2 toggle @Override
356    @Nonnull
357    public HtmlTool getHtmlTool() {
358  2 return requireNonNull(getToolbox("htmlTool", HtmlTool.class), "htmlTool is required");
359    }
360   
361    /**
362    * {@inheritDoc}
363    */
 
364  998 toggle @Override
365    @Nullable public Xpp3Dom get(@Nonnull final String property) {
366  998 requireNonNull(property);
367    // first try page properties
368  998 Xpp3Dom propNode = Xpp3Utils.getFirstChild(pageProperties, property, namespace);
369  998 if (propNode == null) {
370    // try global
371  976 propNode = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
372    }
373   
374  998 return propNode;
375    }
376   
377    /**
378    * Retrieves the text value of the given {@code property}, e.g. as in {@code <myprop>value</myprop>}.
379    *
380    * @param property
381    * the property of interest
382    * @return the configuration value if found in page or globally, {@code null} otherwise.
383    * @see #get(String)
384    * @since 1.0
385    */
 
386  600 toggle @Nullable public String value(@Nonnull final String property) {
387  600 requireNonNull(property);
388  600 final Xpp3Dom propNode = get(property);
389   
390  600 if (propNode == null) {
391    // not found
392  40 return null;
393    }
394   
395  560 return propNode.getValue();
396    }
397   
398    /**
399    * {@inheritDoc}
400    */
 
401  44 toggle @Override
402    @Nullable public <T> T getPropertyValue(@Nonnull final String property,
403    @Nonnull final Class<T> targetType,
404    @Nullable final T defaultValue) {
405  44 return getPropertyValue(property, targetType).orElse(defaultValue);
406    }
407   
408    /**
409    * {@inheritDoc}
410    */
 
411  68 toggle @Override
412    @SuppressWarnings("unchecked")
413    @Nonnull
414    public <T> Optional<T> getPropertyValue(@Nonnull final String property, @Nonnull final Class<T> targetType) {
415  68 requireNonNull(property, "property is required");
416  68 requireNonNull(targetType, "targetType is required");
417  68 final String value = value(property);
418  68 if (value == null) {
419  18 return Optional.empty();
420    }
421  50 Object returnedValue = value;
422  50 if (targetType.isAssignableFrom(Boolean.class)) {
423  22 returnedValue = Boolean.valueOf(value);
424  28 } else if (targetType.isAssignableFrom(Integer.class)) {
425  0 returnedValue = Integer.valueOf(value);
426  28 } else if (targetType.isAssignableFrom(Long.class)) {
427  0 returnedValue = Long.valueOf(value);
428    }
429  50 return Optional.ofNullable((T) returnedValue);
430    }
431   
432    /**
433    * Gets the list of all children name for the {@code parentNode}.
434    *
435    * @param parentNode
436    * the parent node to use (can be {@code null}.
437    * @return Returns a list of {@link String} representing the name of all children, which may be empty but never
438    * {@code null}.
439    * @since 1.3
440    */
 
441  0 toggle public List<String> getChildren(final Xpp3Dom parentNode) {
442  0 return Xpp3Utils.getChildren(parentNode);
443    }
444   
445    /**
446    * {@inheritDoc}
447    */
 
448  308 toggle @Override
449    @Nullable public <T> T getAttributeValue(@Nonnull final String property,
450    @Nonnull final String attribute,
451    @Nonnull final Class<T> targetType,
452    @Nullable final T defaultValue) {
453  308 return getAttributeValue(property, attribute, targetType).orElse(defaultValue);
454    }
455   
456    /**
457    * {@inheritDoc}
458    */
 
459  308 toggle @Override
460    @SuppressWarnings("unchecked")
461    @Nonnull
462    public <T> Optional<T> getAttributeValue(@Nonnull final String property,
463    @Nonnull final String attribute,
464    @Nonnull final Class<T> targetType) {
465  308 requireNonNull(property, "property is required");
466  308 requireNonNull(attribute, "attribute is required");
467  308 requireNonNull(targetType, "targetType is required");
468   
469  308 Xpp3Dom element = get(property);
470  308 if (element == null) {
471  110 return Optional.empty();
472    }
473  198 String value = element.getAttribute(attribute);
474  198 if (value == null) {
475  66 return Optional.empty();
476    }
477   
478  132 if ("inherit".equals(value) || Strings.isNullOrEmpty(value)) {
479  0 element = Xpp3Utils.getFirstChild(globalProperties, property, namespace);
480  0 if (LOGGER.isDebugEnabled()) {
481  0 LOGGER.debug("Inherit value property '{}': {}", property, element);
482    }
483    }
484  132 if (element == null) {
485  0 return Optional.empty();
486    }
487  132 value = element.getAttribute(attribute);
488  132 if (value == null) {
489  0 return Optional.empty();
490    }
491   
492  132 Object returnedValue = value;
493  132 if (targetType.isAssignableFrom(Boolean.class)) {
494  22 returnedValue = Boolean.valueOf(value);
495  110 } else if (targetType.isAssignableFrom(Integer.class)) {
496  0 returnedValue = Integer.valueOf(value);
497  110 } else if (targetType.isAssignableFrom(Long.class)) {
498  0 returnedValue = Long.valueOf(value);
499    }
500  132 return Optional.ofNullable((T) returnedValue);
501    }
502   
503    /**
504    * {@inheritDoc}
505    */
 
506  66 toggle @Override
507    @Nullable public <T> T getAttributeValue(@Nonnull final Xpp3Dom element,
508    @Nonnull final String attribute,
509    @Nonnull final Class<T> targetType,
510    @Nullable final T defaultValue) {
511  66 return getAttributeValue(element, attribute, targetType).orElse(defaultValue);
512    }
513   
514    /**
515    * {@inheritDoc}
516    */
 
517  66 toggle @Override
518    @SuppressWarnings("unchecked")
519    @Nonnull
520    public <T> Optional<T> getAttributeValue(@Nonnull final Xpp3Dom element,
521    @Nonnull final String attribute,
522    @Nonnull final Class<T> targetType) {
523  66 final String value = element.getAttribute(attribute);
524  66 if (value == null) {
525  44 return Optional.empty();
526    }
527  22 Object returnedValue = value;
528  22 if (targetType.isAssignableFrom(Boolean.class)) {
529  0 returnedValue = Boolean.valueOf(value);
530  22 } else if (targetType.isAssignableFrom(Integer.class)) {
531  0 returnedValue = Integer.valueOf(value);
532  22 } else if (targetType.isAssignableFrom(Long.class)) {
533  0 returnedValue = Long.valueOf(value);
534    }
535  22 return Optional.ofNullable((T) returnedValue);
536    }
537   
538    /**
539    * A convenience method to check if the value of the {@code property} is {@code "true"}.
540    *
541    * @param property
542    * the property of interest
543    * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code "true"}.
544    * @see #get(String)
545    * @since 1.0
546    */
 
547  26 toggle public boolean is(final String property) {
548  26 return "true".equals(value(property));
549    }
550   
551    /**
552    * {@inheritDoc}
553    */
 
554  26 toggle @Override
555    public boolean not(final String property) {
556  26 return "false".equals(value(property));
557    }
558   
559    /**
560    * A convenience method to check if the {@code property} is set to a specific value.
561    *
562    * @param property
563    * the property of interest
564    * @param value
565    * the property value to check
566    * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code value}.
567    * @see #get(String)
568    * @since 1.0
569    */
 
570  4 toggle public boolean isValue(final String property, final String value) {
571  4 return value != null && value.equals(value(property));
572    }
573   
574    /**
575    * {@inheritDoc}
576    */
 
577  0 toggle @Override
578    @Nullable public String getProjectId() {
579  0 return projectId;
580    }
581   
582    /**
583    * {@inheritDoc}
584    */
 
585  22 toggle @Override
586    @Nullable public String getFileId() {
587  22 return fileId;
588    }
589   
590    /**
591    * {@inheritDoc}
592    */
 
593  6 toggle @SuppressWarnings("unchecked")
594    @Override
595    @Nonnull
596    public <T extends Context<Context<?>>> T getContext() {
597  6 return (T) context;
598    }
599   
600    /**
601    * {@inheritDoc}
602    */
 
603  46 toggle @Override
604    public org.apache.velocity.context.Context getVelocityContext() {
605  46 return velocityContext;
606    }
607   
608    /**
609    * {@inheritDoc}
610    */
 
611  404 toggle @Override
612    @Nonnull
613    public MavenProject getProject() {
614  404 return project;
615    }
616   
617    /**
618    * {@inheritDoc}
619    */
 
620  54 toggle @Override
621    @Nonnull
622    public SiteModel getSiteModel() {
623  54 return siteModel;
624    }
625   
626    /**
627    * {@inheritDoc}
628    */
 
629  22 toggle @Override
630    @Nonnull
631    public Xpp3Dom getPageProperties() {
632  22 return pageProperties;
633    }
634   
635    /**
636    * {@inheritDoc}
637    */
 
638  24 toggle @Override
639    @Nonnull
640    public Xpp3Dom getGlobalProperties() {
641  24 return globalProperties;
642    }
643   
644    /**
645    * {@inheritDoc}
646    */
 
647  8 toggle @SuppressWarnings("null")
648    @Override
649    @Nonnull
650    public String getShortTitle() {
651  8 String shortTitle = getPropertyValue("shortTitle", String.class).orElse((String) velocityContext.get("shortTitle"));
652    // Page title generation is enabled by default, so check for false and negate
653  8 if (shortTitle == null || shortTitle.isEmpty() || "generate".equalsIgnoreCase(shortTitle)
654    || "true".equalsIgnoreCase(shortTitle)) {
655    // Generate the title.
656    // To generate the title, we take the contents of the first h1 tag in the
657    // document.
658    // If it does not exist, we take the contents of the first h2 tag.
659  0 final HtmlTool htmlTool = getHtmlTool();
660  0 final String body = getBodyContent();
661  0 List<String> hTexts = htmlTool.text(body, "h1");
662  0 if (hTexts.isEmpty()) {
663  0 hTexts = htmlTool.text(body, "h2");
664    }
665  0 if (!hTexts.isEmpty()) {
666  0 shortTitle = hTexts.get(0);
667    }
668    }
669  8 return shortTitle;
670    }
671   
672    /**
673    * {@inheritDoc}
674    */
 
675  8 toggle @Override
676    @Nonnull
677    public String getTitle() {
678  8 final String shortTitle = this.getShortTitle();
679  8 String title = getPropertyValue("title", String.class).orElse((String) velocityContext.get("title"));;
680    // If title template is provided, use it to generate title.
681    // Also regenerate title if a new short title was generated.
682  8 String titleTemplate = getPropertyValue("titleTemplate", String.class).orElse("%1$s - %2$s");
683  8 if (shortTitle != null) {
684  8 final SiteModel model = this.getSiteModel();
685    // the company/project name is retrieved from site model ( <project name="Foo">)
686    // or from the project itself
687  8 String titleProjectName = title;
688  8 if (titleProjectName == null || titleProjectName.isEmpty()) {
689  4 titleProjectName = null;
690  4 if (model.getName() != null) {
691  2 titleProjectName = model.getName();
692  2 } else if (getProject() != null && getProject().getName() != null) {
693  2 titleProjectName = getProject().getName();
694    }
695    }
696   
697  8 if (titleProjectName == null) {
698  0 return shortTitle;
699    }
700    // use String.format() to create new title
701  8 try {
702    // try to format the title according to the template
703  8 title = String.format(titleTemplate, titleProjectName, shortTitle);
704    } catch (final Exception e) {
705  0 LOGGER.warn(
706    "Cannot format title with template '{}' and tiltle '{}' and short title '{}', falling back to default format.",
707    titleTemplate,
708    titleProjectName,
709    title,
710    e);
711    }
712    }
713  8 return title;
714    }
715   
716    /**
717    * {@inheritDoc}
718    */
 
719  0 toggle @SuppressWarnings("null")
720    @Override
721    @Nonnull
722    public String getPublishDate() {
723  0 final SiteModel model = this.getSiteModel();
724  0 final PublishDate publishDate = model.getPublishDate();
725  0 String publishDateValue = null;
726  0 String format = "yyyy-MM-dd";
727  0 if (!"none".equals(publishDate.getPosition())) {
728  0 if (publishDate.getFormat() != null && !publishDate.getFormat().isEmpty()) {
729  0 format = publishDate.getFormat();
730    }
731  0 Date outputTimestamp = null;
732  0 try {
733  0 outputTimestamp = getBuildOutputTimestamp();
734    } catch (final ParseException e) {
735  0 LOGGER.warn("Cannot parse the build output timestamp!", e);
736  0 return null;
737    }
738   
739  0 final DateTool dateTool = getToolbox("date", DateTool.class);
740  0 if (dateTool != null) {
741  0 if (outputTimestamp != null) {
742  0 publishDateValue = dateTool.format(format, outputTimestamp);
743    } else {
744  0 publishDateValue = dateTool.get(format);
745    }
746    }
747    }
748  0 return publishDateValue;
749    }
750   
751    /**
752    * {@inheritDoc}
753    */
 
754  0 toggle @SuppressWarnings("null")
755    @Override
756    @Nonnull
757    public String getPublishDateMessage() {
758  0 final String publishDate = getPublishDate();
759  0 if (Strings.isNullOrEmpty(publishDate)) {
760  0 return null;
761    }
762  0 final ResourceTool text = this.getToolbox("text", ResourceTool.class);
763  0 if (text == null) {
764  0 return null;
765    }
766  0 return text.get("template.lastpublished") + ": " + publishDate;
767    }
768   
769    /**
770    * {@inheritDoc}
771    */
 
772  0 toggle @Override
773    @Nonnull
774    public String getDatePosition() {
775    // resolve date position, or set the default otherwise (bottom)
776    // Note, that currently we do not support "navigation-top" position for
777    // either publishDate or version.
778  0 PositionType datePosition = PositionType.none;
779  0 final SiteModel model = this.getSiteModel();
780  0 if (model != null) {
781  0 final PublishDate publishDate = model.getPublishDate();
782  0 if (publishDate != null) {
783  0 final String position = publishDate.getPosition();
784  0 if (position != null && !position.isEmpty()) {
785  0 datePosition = PositionType.valueOfHyphen(position);
786    }
787    }
788    }
789  0 if (datePosition == null) {
790  0 datePosition = PositionType.none;
791    }
792  0 return datePosition.toString();
793    }
794   
795    /**
796    * {@inheritDoc}
797    */
 
798  0 toggle @Override
799    @Nonnull
800    public String getVersion() {
801  0 final MavenProject project = this.getProject();
802  0 return project.getVersion();
803    }
804   
805    /**
806    * {@inheritDoc}
807    */
 
808  0 toggle @Override
809    @Nonnull
810    public String getVersionPosition() {
811  0 final SiteModel model = this.getSiteModel();
812  0 if (model == null || model.getVersion() == null) {
813  0 return PositionType.none.toString();
814    }
815  0 PositionType position = PositionType.valueOfHyphen(model.getVersion().getPosition());
816  0 if (position == null) {
817  0 position = PositionType.none;
818    }
819  0 return position.toString();
820    }
821   
822    /**
823    * {@inheritDoc}
824    */
 
825  0 toggle @SuppressWarnings("null")
826    @Override
827    @Nonnull
828    public String getVersionMessage() {
829  0 final String version = getVersion();
830  0 if (Strings.isNullOrEmpty(version)) {
831  0 return null;
832    }
833  0 final ResourceTool text = this.getToolbox("text", ResourceTool.class);
834  0 if (text == null) {
835  0 return null;
836    }
837  0 return text.get("template.version") + ": " + version;
838    }
839   
840    /**
841    * {@inheritDoc}
842    */
 
843  66 toggle @Override
844    @Nonnull
845    public String getNamespace() {
846  66 return namespace;
847    }
848   
849    /**
850    * @return Returns the html body content stored in velocity context.
851    * @since 2.1
852    */
 
853  0 toggle @Override
854    @Nonnull
855    public String getBodyContent() {
856  0 return requireNonNull(getContextValue("bodyContent", String.class));
857    }
858   
859    /**
860    * @return Returns the location of project.
861    */
 
862  376 toggle @Nonnull
863    public String getProjectLocation() {
864  376 String projectSiteLoc = getProject().getUrl();
865  376 if (!Strings.isNullOrEmpty(projectSiteLoc)) {
866   
867  374 if (!projectSiteLoc.endsWith("/")) {
868  0 projectSiteLoc += "/";
869    }
870    } else {
871  2 LOGGER.warn("the property project.url is required");
872  2 projectSiteLoc = "https://localhost/";
873    }
874  376 return projectSiteLoc;
875    }
876   
877    /**
878    * <p>
879    * See <a href= "https://maven.apache.org/doxia/doxia-sitetools/doxia-site-renderer/">Doxia Sitetools - Site
880    * Renderer</a> for more information.
881    *
882    * @return Returns a {@link String} representing the name of current file of the (HTML) document being rendered,
883    * relative to the site root.
884    */
 
885  396 toggle @Nonnull
886    public String getCurrentFileName() {
887  396 return (String) velocityContext.get("currentFileName");
888    }
889   
890    /**
891    * @return Returns a {@link String} representing the location path of current rendered file.
892    */
 
893  352 toggle @Nonnull
894    public String getCurrentFileLocation() {
895  352 final String projectSiteLoc = getProjectLocation();
896  352 return URITool.toURI(projectSiteLoc).resolve(getCurrentFileName()).toString();
897    }
898   
899    /**
900    * {@inheritDoc}
901    */
 
902  22 toggle @Override
903    @SuppressWarnings("unchecked")
904    @Nullable public <T> T eval(@Nullable final String vtl, @Nonnull final Class<T> requiredClass) {
905  22 if (vtl == null) {
906  0 return null;
907    }
908  22 final RenderTool renderTool = (RenderTool) getVelocityContext().get("render");
909  22 try {
910  22 return (T) renderTool.eval(getVelocityContext(), vtl);
911    } catch (final Exception ex) {
912  0 throw new RuntimeException("error when try evaluate '" + vtl + "'", ex);
913    }
914    }
915   
916    /**
917    * {@inheritDoc}
918    */
 
919  440 toggle @Override
920    public String relativeLink(final String href) {
921  440 if (href == null) {
922  0 return null;
923    }
924  440 if (isExternalLink(href)) {
925  88 return href;
926    }
927  352 final String relativePath = (String) velocityContext.get("relativePath");
928  352 String relativeLink = PathTool.calculateLink(href, relativePath);
929  352 relativeLink = relativeLink.replaceAll("\\\\", "/");
930  352 if (Strings.isNullOrEmpty(relativeLink)) {
931  0 relativeLink = "./";
932    }
933    // Attempt to normalise the relative link - this is useful for active link
934    // calculations and better relative links for subdirectories.
935    //
936    // The issue is particularly visible with pages in subdirectories,
937    // so that if you are in <root>/dev/index.html, the relative menu link to
938    // the _same_ page would likely be ../dev/index.html instead of '' or
939    // 'index.html'.
940  352 final String currentFileLoc = getCurrentFileLocation();
941  352 final String absoluteLink = URITool.toURI(currentFileLoc).resolve(relativeLink).normalize().toString();
942  352 if (currentFileLoc.equals(absoluteLink)) {
943    // for matching link, use empty relative link
944  0 relativeLink = StringUtils.EMPTY;
945    } else {
946    // relativize the absolute link based on current directory
947    // (uses Maven project link relativization)
948  352 final String currentFileDir = PathTool.getDirectoryComponent(currentFileLoc);
949  352 relativeLink = URITool.relativizeLink(currentFileDir, absoluteLink);
950    }
951  352 if (LOGGER.isTraceEnabled()) {
952  0 LOGGER.trace("-- Relative Link ----------------------------------");
953  0 LOGGER.trace("link: {}", href);
954  0 LOGGER.trace("currentFileLoc: {}", currentFileLoc);
955  0 LOGGER.trace("absoluteLink: {}", absoluteLink);
956  0 LOGGER.trace("relativeLink: {}", relativeLink);
957  0 LOGGER.trace("---------------------------------------------------");
958    }
959  352 return relativeLink;
960    }
961   
962    /**
963    * @param url
964    * a url.
965    * @return Returns {@code true} whether the link is a external link to the site.
966    */
 
967  456 toggle @SuppressWarnings("null")
968    @Override
969    public boolean isExternalLink(final String url) {
970  456 if (url == null) {
971  2 return false;
972    }
973  454 final String absoluteResourceURL = this.value("absoluteResourceURL");
974  454 if (!Strings.isNullOrEmpty(absoluteResourceURL) && url.startsWith(absoluteResourceURL)) {
975  24 return false;
976    }
977  430 return url.toLowerCase().startsWith("http:/") || url.toLowerCase().startsWith("https:/")
978    || url.toLowerCase().startsWith("ftp:/") || url.toLowerCase().startsWith("mailto:")
979    || url.toLowerCase().startsWith("file:/") || url.toLowerCase().indexOf("://") != -1;
980    }
981   
982    /**
983    * {@inheritDoc}
984    */
 
985  362 toggle @Override
986    public boolean isActiveLink(@Nullable final String href) {
987  362 final String alignedFileName = (String) velocityContext.get("alignedFileName");
988  362 if (href == null) {
989  2 return false;
990    }
991    // either empty link (pointing to a page), or if the current file is index.html,
992    // the link may point to the whole directory
993  360 return Strings.isNullOrEmpty(href) || alignedFileName.endsWith("index.html") && ".".equals(href);
994    }
995   
996    /**
997    * Converts a filename to pageId format.
998    *
999    * @param fileName
1000    * the filename to convert
1001    * @return Returns a {@link String} representing the pageId of {@code filename}.
1002    */
 
1003  100 toggle @Nullable public static String slugFilename(@Nullable final String fileName) {
1004  100 if (fileName == null) {
1005  0 return null;
1006    }
1007  100 String currentFile = fileName;
1008   
1009    // drop the extension
1010  100 final int lastDot = currentFile.lastIndexOf(".");
1011  100 if (lastDot >= 0) {
1012  98 currentFile = currentFile.substring(0, lastDot);
1013    }
1014   
1015    // get the short ID (in case of nested files)
1016    // String fileName = new File(currentFile).getName();
1017    // fileShortId = HtmlTool.slug(fileName);
1018   
1019    // full file ID includes the nested dirs
1020    // replace nesting "/" with "-"
1021  100 return HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-"));
1022    }
1023   
1024    /**
1025    * @return Returns a {@link String} representing the relative path to root site.
1026    */
 
1027  22 toggle @SuppressWarnings("null")
1028    @Override
1029    @Nonnull
1030    public String getResourcePath() {
1031  22 final String absoluteResourceURL = this.value("absoluteResourceURL");
1032  22 String projectUrl = getProjectLocation();
1033  22 final String currentFileName = getCurrentFileName();
1034  22 if (!Strings.isNullOrEmpty(projectUrl) && currentFileName != null) {
1035  22 if (projectUrl.charAt(projectUrl.length() - 1) != '/') {
1036  0 projectUrl += '/';
1037    }
1038  22 final String currentFileDir = URITool.toURI(projectUrl).resolve(currentFileName).resolve(".").toString();
1039  22 return URITool.relativizeLink(currentFileDir, absoluteResourceURL);
1040    }
1041  0 return (String) velocityContext.get("relativePath");
1042    }
1043   
1044    /**
1045    * Gets the reproduce build timestamp whether property 'project.build.outputTimestamp' is fill in.
1046    *
1047    * @return Returns a instance of {@code Date} representing the reproduce build timestamp whether property
1048    * 'project.build.outputTimestamp' is fill in.
1049    */
 
1050  0 toggle @Override
1051    @Nullable public Date getBuildOutputTimestamp() throws ParseException {
1052  0 if (!this.velocityContext.containsKey(PROJECT_BUILD_OUTPUTTIMESTAMP)) {
1053  0 return null;
1054    }
1055  0 final Object outputTimestamp = this.velocityContext.get(PROJECT_BUILD_OUTPUTTIMESTAMP);
1056  0 if (outputTimestamp != null) {
1057  0 return ISO_8601BASIC_DATE.parse(outputTimestamp.toString());
1058    }
1059  0 return null;
1060    }
1061   
1062    }