Class |
Line # |
Actions |
|||||
---|---|---|---|---|---|---|---|
SkinConfigTool | 91 | 211 | 0% | 94 | 124 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one | |
3 | * or more contributor license agreements. See the NOTICE file | |
4 | * distributed with this work for additional information | |
5 | * regarding copyright ownership. The ASF licenses this file | |
6 | * to you under the Apache License, Version 2.0 (the | |
7 | * "License"); you may not use this file except in compliance | |
8 | * with the License. You may obtain a copy of the License at | |
9 | * | |
10 | * http://www.apache.org/licenses/LICENSE-2.0 | |
11 | * | |
12 | * Unless required by applicable law or agreed to in writing, | |
13 | * software distributed under the License is distributed on an | |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
15 | * KIND, either express or implied. See the License for the | |
16 | * specific language governing permissions and limitations | |
17 | * under the License. | |
18 | */ | |
19 | package org.devacfr.maven.skins.reflow; | |
20 | ||
21 | import javax.annotation.Nonnull; | |
22 | import javax.annotation.Nullable; | |
23 | ||
24 | import java.text.ParseException; | |
25 | import java.text.SimpleDateFormat; | |
26 | import java.util.Date; | |
27 | import java.util.List; | |
28 | ||
29 | import com.google.common.base.Strings; | |
30 | import org.apache.commons.lang3.StringUtils; | |
31 | import org.apache.maven.doxia.site.SiteModel; | |
32 | import org.apache.maven.project.MavenProject; | |
33 | import org.apache.velocity.tools.ToolContext; | |
34 | import org.apache.velocity.tools.config.DefaultKey; | |
35 | import org.apache.velocity.tools.generic.RenderTool; | |
36 | import org.apache.velocity.tools.generic.SafeConfig; | |
37 | import org.apache.velocity.tools.generic.ValueParser; | |
38 | import org.codehaus.plexus.util.PathTool; | |
39 | import org.codehaus.plexus.util.xml.Xpp3Dom; | |
40 | import org.devacfr.maven.skins.reflow.context.Context; | |
41 | import org.slf4j.Logger; | |
42 | import org.slf4j.LoggerFactory; | |
43 | ||
44 | import static java.util.Objects.requireNonNull; | |
45 | ||
46 | /** | |
47 | * An Apache Velocity tool that simplifies retrieval of custom configuration values for a Maven Site. | |
48 | * <p> | |
49 | * The tool is configured to access Maven site configuration of a skin inside {@code <custom>} element of site | |
50 | * descriptor. It supports global properties (defined at skin level) and per-page properties (defined in | |
51 | * {@code <page><mypage>} element). The per-page properties override the global ones. | |
52 | * </p> | |
53 | * <p> | |
54 | * A sample configuration would be like that: | |
55 | * </p> | |
56 | * | |
57 | * <pre> | |
58 | * {@code | |
59 | * <custom> | |
60 | * <reflowSkin> | |
61 | * <prop1>value1</prop1> | |
62 | * <prop2> | |
63 | * <prop21>value2</prop21> | |
64 | * </prop2> | |
65 | * <pages> | |
66 | * <mypage project="myproject"> | |
67 | * <prop1>override value1</prop1> | |
68 | * </mypage> | |
69 | * </pages> | |
70 | * </reflowSkin> | |
71 | * </custom> | |
72 | * } | |
73 | * </pre> | |
74 | * <p> | |
75 | * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return "override value1". | |
76 | * Then {@code $config.prop2} would return "value2" - the global value. | |
77 | * </p> | |
78 | * <p> | |
79 | * The tool allows querying the value easily, falling back from page to global configuration to {@code null}, if none is | |
80 | * available. It also provides convenience accessors for common values. | |
81 | * </p> | |
82 | * <p> | |
83 | * Note | |
84 | * </p> | |
85 | * | |
86 | * @author Andrius Velykis | |
87 | * @author Christophe Friederich | |
88 | * @since 1.0 | |
89 | */ | |
90 | @DefaultKey("config") | |
91 | public class SkinConfigTool extends SafeConfig implements ISkinConfig { | |
92 | ||
93 | /** */ | |
94 | private static final Logger LOGGER = LoggerFactory.getLogger(SkinConfigTool.class); | |
95 | ||
96 | // ISO 8601 BASIC is used by build timestamp | |
97 | private static final SimpleDateFormat ISO_8601BASIC_DATE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); | |
98 | ||
99 | private static final String PROJECT_BUILD_OUTPUTTIMESTAMP = "project.build.outputTimestamp"; | |
100 | ||
101 | /** */ | |
102 | public static final String DEFAULT_KEY = "config"; | |
103 | ||
104 | /** By default use Reflow skin configuration tag. */ | |
105 | public static final String SKIN_KEY = "reflowSkin"; | |
106 | ||
107 | /** */ | |
108 | private String key = DEFAULT_KEY; | |
109 | ||
110 | /** */ | |
111 | private String skinKey = SKIN_KEY; | |
112 | ||
113 | /** Create dummy nodes to avoid null checks. */ | |
114 | private Xpp3Dom globalProperties = new Xpp3Dom(""); | |
115 | ||
116 | /** */ | |
117 | private Xpp3Dom pageProperties = new Xpp3Dom(""); | |
118 | ||
119 | /** */ | |
120 | private String namespace = ""; | |
121 | ||
122 | /** */ | |
123 | private String projectId = null; | |
124 | ||
125 | /** */ | |
126 | private String fileId = null; | |
127 | ||
128 | /** */ | |
129 | private Context<?> context = null; | |
130 | ||
131 | /** */ | |
132 | private MavenProject project = null; | |
133 | ||
134 | /** */ | |
135 | private SiteModel siteModel; | |
136 | ||
137 | /** */ | |
138 | private ToolContext velocityContext; | |
139 | ||
140 | /** | |
141 | * {@inheritDoc} | |
142 | * | |
143 | * @see SafeConfig#configure(ValueParser) | |
144 | */ | |
145 | 6 | @Override |
146 | protected void configure(final ValueParser values) { | |
147 | 6 | final String altkey = values.getString("key"); |
148 | 6 | if (altkey != null) { |
149 | 0 | setKey(altkey); |
150 | } | |
151 | ||
152 | // allow changing skin key in the configuration | |
153 | 6 | final String altSkinKey = values.getString("skinKey"); |
154 | 6 | if (altSkinKey != null) { |
155 | 0 | this.skinKey = altSkinKey; |
156 | } | |
157 | ||
158 | // retrieve the site model from Velocity context | |
159 | 6 | final Object vc = values.get("velocityContext"); |
160 | ||
161 | 6 | if (!(vc instanceof ToolContext)) { |
162 | 0 | return; |
163 | } | |
164 | ||
165 | 6 | this.velocityContext = (ToolContext) vc; |
166 | ||
167 | 6 | final Object projectObj = velocityContext.get("project"); |
168 | 6 | if (projectObj instanceof MavenProject) { |
169 | 6 | this.project = (MavenProject) projectObj; |
170 | 6 | final String artifactId = project.getArtifactId(); |
171 | // use artifactId "sluggified" as the projectId | |
172 | 6 | projectId = HtmlTool.slug(artifactId); |
173 | } | |
174 | ||
175 | // calculate the page ID from the current file name | |
176 | 6 | final String currentFileObj = getCurrentFileName(); |
177 | 6 | fileId = slugFilename(currentFileObj); |
178 | ||
179 | 6 | final Object siteModelObj = velocityContext.get("site"); |
180 | ||
181 | 6 | if (!(siteModelObj instanceof SiteModel)) { |
182 | 0 | return; |
183 | } | |
184 | ||
185 | 6 | this.siteModel = (SiteModel) siteModelObj; |
186 | 6 | final Object customObj = siteModel.getCustom(); |
187 | ||
188 | 6 | if (!(customObj instanceof Xpp3Dom)) { |
189 | 0 | return; |
190 | } | |
191 | ||
192 | // Now that we have the custom node, get the global properties | |
193 | // under the skin tag | |
194 | 6 | final Xpp3Dom customNode = (Xpp3Dom) customObj; |
195 | 6 | Xpp3Dom skinNode = customNode.getChild(skinKey); |
196 | 6 | final String namespaceKey = ":" + skinKey; |
197 | ||
198 | 6 | if (skinNode == null) { |
199 | // try searching with any namespace | |
200 | 0 | for (final Xpp3Dom child : customNode.getChildren()) { |
201 | 0 | if (child.getName().endsWith(namespaceKey)) { |
202 | 0 | skinNode = child; |
203 | 0 | break; |
204 | } | |
205 | } | |
206 | } | |
207 | ||
208 | 6 | if (skinNode != null) { |
209 | 6 | globalProperties = skinNode; |
210 | ||
211 | 6 | if (skinNode.getName().endsWith(namespaceKey)) { |
212 | // extract the namespace (including the colon) | |
213 | 0 | namespace = Strings.emptyToNull( |
214 | skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1)); | |
215 | } | |
216 | ||
217 | // for page properties, retrieve the file name and drop the `.html` | |
218 | // extension - this will be used, i.e. `index` instead of `index.html` | |
219 | 6 | final Xpp3Dom pagesNode = Xpp3Utils.getFirstChild(skinNode, "pages", namespace); |
220 | 6 | if (pagesNode != null) { |
221 | ||
222 | // Get the page for the file | |
223 | 6 | Xpp3Dom page = Xpp3Utils.getFirstChild(pagesNode, fileId, namespace); |
224 | ||
225 | // Now check if the project artifact ID is set, and if so, if it matches the | |
226 | // current project. This allows preventing accidental reuse of parent page | |
227 | // configs in children modules | |
228 | 6 | if (page != null && projectId != null) { |
229 | 6 | final String pageProject = page.getAttribute("project"); |
230 | 6 | if (pageProject != null && !projectId.equals(pageProject)) { |
231 | // project ID indicated, and is different - do not use the config | |
232 | 0 | page = null; |
233 | } | |
234 | } | |
235 | ||
236 | 6 | if (page != null) { |
237 | 6 | pageProperties = page; |
238 | } | |
239 | } | |
240 | ||
241 | // Config option <localResources>true</localResources> to force CDN-less | |
242 | // Bootstrap & jQuery | |
243 | 6 | this.velocityContext.put("localResources", is("localResources")); |
244 | // Use config option | |
245 | // <absoluteResourceURL>http://mysite.com/</absoluteResourceURL> | |
246 | 6 | this.velocityContext.put("resourcePath", getResourcePath()); |
247 | ||
248 | 6 | this.context = Context.buildContext(this); |
249 | } | |
250 | 6 | if (LOGGER.isDebugEnabled()) { |
251 | 0 | LOGGER.debug("Current Filename: {}", currentFileObj); |
252 | 0 | LOGGER.debug("Project id: {}", projectId); |
253 | 0 | LOGGER.debug("File id: {}", fileId); |
254 | 0 | LOGGER.debug("Context: {}", this.context); |
255 | 0 | LOGGER.debug("Namespace: {}", this.namespace); |
256 | 0 | LOGGER.debug("---------------------------------------------------"); |
257 | } | |
258 | } | |
259 | ||
260 | /** | |
261 | * Sets the key under which this tool has been configured. | |
262 | * | |
263 | * @param key | |
264 | * the key of config | |
265 | * @since 1.0 | |
266 | */ | |
267 | 0 | protected void setKey(final String key) { |
268 | 0 | this.key = requireNonNull(key, "SkinConfigTool key cannot be null"); |
269 | } | |
270 | ||
271 | /** | |
272 | * @return Returns the key under which this tool has been configured. The default is `config`. | |
273 | * @since 1.0 | |
274 | */ | |
275 | 0 | public String getKey() { |
276 | 0 | return this.key; |
277 | } | |
278 | ||
279 | /** | |
280 | * {@inheritDoc} | |
281 | */ | |
282 | 0 | @Override |
283 | @Nullable public <T> T getContextValue(@Nonnull final String key, @Nonnull final Class<T> type) { | |
284 | 0 | requireNonNull(type); |
285 | 0 | if (String.class.isAssignableFrom(type)) { |
286 | 0 | return this.eval("$" + key, type); |
287 | } else { | |
288 | 0 | throw new UnsupportedOperationException(); |
289 | } | |
290 | } | |
291 | ||
292 | /** | |
293 | * {@inheritDoc} | |
294 | */ | |
295 | 0 | @Override |
296 | public void setContextValue(@Nonnull final String key, @Nullable final Object value) { | |
297 | 0 | requireNonNull(key); |
298 | 0 | if (value instanceof String) { |
299 | 0 | this.eval("#set( $" + key + "= \"" + value.toString() + "\")", Void.class); |
300 | } else { | |
301 | 0 | throw new UnsupportedOperationException(); |
302 | } | |
303 | } | |
304 | ||
305 | /** | |
306 | * {@inheritDoc} | |
307 | */ | |
308 | 0 | @Override |
309 | @Nullable @SuppressWarnings("unchecked") | |
310 | public <T> T getToolbox(@Nonnull final String toolName, @Nonnull final Class<T> toolType) { | |
311 | 0 | requireNonNull(toolType); |
312 | 0 | return (T) this.velocityContext.getToolbox().get(requireNonNull(toolName)); |
313 | } | |
314 | ||
315 | /** | |
316 | * {@inheritDoc} | |
317 | */ | |
318 | 271 | @Override |
319 | @Nullable public Xpp3Dom get(@Nonnull final String property) { | |
320 | 271 | requireNonNull(property); |
321 | // first try page properties | |
322 | 271 | Xpp3Dom propNode = Xpp3Utils.getFirstChild(pageProperties, property, namespace); |
323 | 271 | if (propNode == null) { |
324 | // try global | |
325 | 265 | propNode = Xpp3Utils.getFirstChild(globalProperties, property, namespace); |
326 | } | |
327 | ||
328 | 271 | return propNode; |
329 | } | |
330 | ||
331 | /** | |
332 | * Retrieves the text value of the given {@code property}, e.g. as in {@code <myprop>value</myprop>}. | |
333 | * | |
334 | * @param property | |
335 | * the property of interest | |
336 | * @return the configuration value if found in page or globally, {@code null} otherwise. | |
337 | * @see #get(String) | |
338 | * @since 1.0 | |
339 | */ | |
340 | 163 | @Nullable public String value(@Nonnull final String property) { |
341 | 163 | requireNonNull(property); |
342 | 163 | final Xpp3Dom propNode = get(property); |
343 | ||
344 | 163 | if (propNode == null) { |
345 | // not found | |
346 | 6 | return null; |
347 | } | |
348 | ||
349 | 157 | return propNode.getValue(); |
350 | } | |
351 | ||
352 | /** | |
353 | * Gets the text value of the given {@code property}. | |
354 | * | |
355 | * @param property | |
356 | * the property to use | |
357 | * @param targetType | |
358 | * the returned target type use to convert value. | |
359 | * @param defaultValue | |
360 | * the default value used if property doesn't exist. | |
361 | * @return Returns a converted value of the given {@code property}. | |
362 | * @since 2.0 | |
363 | * @param <T> | |
364 | * the type of returned object. | |
365 | */ | |
366 | 12 | @Override |
367 | @SuppressWarnings("unchecked") | |
368 | @Nullable public <T> T getPropertyValue(@Nonnull final String property, | |
369 | @Nonnull final Class<T> targetType, | |
370 | @Nullable final T defaultValue) { | |
371 | 12 | requireNonNull(property, "property is required"); |
372 | 12 | requireNonNull(targetType, "targetType is required"); |
373 | 12 | final String value = value(property); |
374 | 12 | if (value == null) { |
375 | 0 | return defaultValue; |
376 | } | |
377 | 12 | Object returnedValue = value; |
378 | 12 | if (targetType.isAssignableFrom(Boolean.class)) { |
379 | 6 | returnedValue = Boolean.valueOf(value); |
380 | 6 | } else if (targetType.isAssignableFrom(Integer.class)) { |
381 | 0 | returnedValue = Integer.valueOf(value); |
382 | 6 | } else if (targetType.isAssignableFrom(Long.class)) { |
383 | 0 | returnedValue = Long.valueOf(value); |
384 | } | |
385 | 12 | return (T) returnedValue; |
386 | } | |
387 | ||
388 | /** | |
389 | * Gets the list of all children name for the {@code parentNode}. | |
390 | * | |
391 | * @param parentNode | |
392 | * the parent node to use (can be {@code null}. | |
393 | * @return Returns a list of {@link String} representing the name of all children, which may be empty but never | |
394 | * {@code null}. | |
395 | * @since 1.3 | |
396 | */ | |
397 | 0 | public List<String> getChildren(final Xpp3Dom parentNode) { |
398 | 0 | return Xpp3Utils.getChildren(parentNode); |
399 | } | |
400 | ||
401 | /** | |
402 | * Gets the attribute value of the given {@code attribute} of {@code property}. | |
403 | * | |
404 | * @param property | |
405 | * the property to use | |
406 | * @param attribute | |
407 | * the attribute to use. | |
408 | * @param targetType | |
409 | * the returned target type use to convert value. | |
410 | * @param defaultValue | |
411 | * the default value used if property doesn't exist. | |
412 | * @return Returns a converted value of the given {@code property}. | |
413 | * @since 2.0 | |
414 | * @param <T> | |
415 | * the type of returned object. | |
416 | */ | |
417 | 84 | @Override |
418 | @SuppressWarnings("unchecked") | |
419 | @Nullable public <T> T getAttributeValue(@Nonnull final String property, | |
420 | @Nonnull final String attribute, | |
421 | @Nonnull final Class<T> targetType, | |
422 | @Nullable final T defaultValue) { | |
423 | 84 | requireNonNull(property, "property is required"); |
424 | 84 | requireNonNull(attribute, "attribute is required"); |
425 | 84 | requireNonNull(targetType, "targetType is required"); |
426 | ||
427 | 84 | Xpp3Dom element = get(property); |
428 | 84 | if (element == null) { |
429 | 30 | return defaultValue; |
430 | } | |
431 | 54 | String value = element.getAttribute(attribute); |
432 | 54 | if (value == null) { |
433 | 18 | return defaultValue; |
434 | } | |
435 | ||
436 | 36 | if ("inherit".equals(value) || Strings.isNullOrEmpty(value)) { |
437 | 0 | element = Xpp3Utils.getFirstChild(globalProperties, property, namespace); |
438 | 0 | if (LOGGER.isDebugEnabled()) { |
439 | 0 | LOGGER.debug("Inherit value property '{}': {}", property, element); |
440 | } | |
441 | } | |
442 | 36 | if (element == null) { |
443 | 0 | return defaultValue; |
444 | } | |
445 | 36 | value = element.getAttribute(attribute); |
446 | 36 | if (value == null) { |
447 | 0 | return defaultValue; |
448 | } | |
449 | ||
450 | 36 | Object returnedValue = value; |
451 | 36 | if (targetType.isAssignableFrom(Boolean.class)) { |
452 | 6 | returnedValue = Boolean.valueOf(value); |
453 | 30 | } else if (targetType.isAssignableFrom(Integer.class)) { |
454 | 0 | returnedValue = Integer.valueOf(value); |
455 | 30 | } else if (targetType.isAssignableFrom(Long.class)) { |
456 | 0 | returnedValue = Long.valueOf(value); |
457 | } | |
458 | 36 | return (T) returnedValue; |
459 | } | |
460 | ||
461 | /** | |
462 | * {@inheritDoc} | |
463 | */ | |
464 | 18 | @Override |
465 | @SuppressWarnings("unchecked") | |
466 | @Nullable public <T> T getAttributeValue(@Nonnull final Xpp3Dom element, | |
467 | @Nonnull final String attribute, | |
468 | @Nonnull final Class<T> targetType, | |
469 | @Nullable final T defaultValue) { | |
470 | 18 | if (element == null) { |
471 | 0 | return defaultValue; |
472 | } | |
473 | 18 | final String value = element.getAttribute(attribute); |
474 | 18 | if (value == null) { |
475 | 12 | return defaultValue; |
476 | } | |
477 | 6 | Object returnedValue = value; |
478 | 6 | if (targetType.isAssignableFrom(Boolean.class)) { |
479 | 0 | returnedValue = Boolean.valueOf(value); |
480 | 6 | } else if (targetType.isAssignableFrom(Integer.class)) { |
481 | 0 | returnedValue = Integer.valueOf(value); |
482 | 6 | } else if (targetType.isAssignableFrom(Long.class)) { |
483 | 0 | returnedValue = Long.valueOf(value); |
484 | } | |
485 | 6 | return (T) returnedValue; |
486 | } | |
487 | ||
488 | /** | |
489 | * A convenience method to check if the value of the {@code property} is {@code "true"}. | |
490 | * | |
491 | * @param property | |
492 | * the property of interest | |
493 | * @return {@code true} if the configuration value is set either in page or globally, and is equal to | |
494 | * {@code "true"}. | |
495 | * @see #get(String) | |
496 | * @since 1.0 | |
497 | */ | |
498 | 8 | public boolean is(final String property) { |
499 | 8 | return "true".equals(value(property)); |
500 | } | |
501 | ||
502 | /** | |
503 | * {@inheritDoc} | |
504 | */ | |
505 | 8 | @Override |
506 | public boolean not(final String property) { | |
507 | 8 | return "false".equals(value(property)); |
508 | } | |
509 | ||
510 | /** | |
511 | * A convenience method to check if the {@code property} is set to a specific value. | |
512 | * | |
513 | * @param property | |
514 | * the property of interest | |
515 | * @param value | |
516 | * the property value to check | |
517 | * @return {@code true} if the configuration value is set either in page or globally, and is equal to {@code value}. | |
518 | * @see #get(String) | |
519 | * @since 1.0 | |
520 | */ | |
521 | 2 | public boolean isValue(final String property, final String value) { |
522 | 2 | return value != null && value.equals(value(property)); |
523 | } | |
524 | ||
525 | /** | |
526 | * {@inheritDoc} | |
527 | */ | |
528 | 0 | @Override |
529 | @Nullable public String getProjectId() { | |
530 | 0 | return projectId; |
531 | } | |
532 | ||
533 | /** | |
534 | * {@inheritDoc} | |
535 | */ | |
536 | 6 | @Override |
537 | @Nullable public String getFileId() { | |
538 | 6 | return fileId; |
539 | } | |
540 | ||
541 | /** | |
542 | * @return the context | |
543 | */ | |
544 | 0 | @Override |
545 | @Nonnull | |
546 | public Context<?> getContext() { | |
547 | 0 | return context; |
548 | } | |
549 | ||
550 | /** | |
551 | * @return the velocity Context | |
552 | */ | |
553 | 12 | public ToolContext getVelocityContext() { |
554 | 12 | return velocityContext; |
555 | } | |
556 | ||
557 | /** | |
558 | * {@inheritDoc} | |
559 | */ | |
560 | 108 | @Override |
561 | @Nonnull | |
562 | public MavenProject getProject() { | |
563 | 108 | return project; |
564 | } | |
565 | ||
566 | /** | |
567 | * {@inheritDoc} | |
568 | */ | |
569 | 12 | @Override |
570 | @Nonnull | |
571 | public SiteModel getSiteModel() { | |
572 | 12 | return siteModel; |
573 | } | |
574 | ||
575 | /** | |
576 | * {@inheritDoc} | |
577 | */ | |
578 | 6 | @Override |
579 | @Nonnull | |
580 | public Xpp3Dom getPageProperties() { | |
581 | 6 | return pageProperties; |
582 | } | |
583 | ||
584 | /** | |
585 | * {@inheritDoc} | |
586 | */ | |
587 | 6 | @Override |
588 | @Nonnull | |
589 | public Xpp3Dom getGlobalProperties() { | |
590 | 6 | return globalProperties; |
591 | } | |
592 | ||
593 | /** | |
594 | * {@inheritDoc} | |
595 | */ | |
596 | 18 | @Override |
597 | @Nonnull | |
598 | public String getNamespace() { | |
599 | 18 | return namespace; |
600 | } | |
601 | ||
602 | /** | |
603 | * @return Returns the location of project. | |
604 | */ | |
605 | 102 | @Nonnull |
606 | public String getProjectLocation() { | |
607 | 102 | String projectSiteLoc = getProject().getUrl(); |
608 | 102 | if (!Strings.isNullOrEmpty(projectSiteLoc)) { |
609 | ||
610 | 102 | if (!projectSiteLoc.endsWith("/")) { |
611 | 0 | projectSiteLoc += "/"; |
612 | } | |
613 | } | |
614 | 102 | return projectSiteLoc; |
615 | } | |
616 | ||
617 | /** | |
618 | * <p> | |
619 | * See <a href= "https://maven.apache.org/doxia/doxia-sitetools/doxia-site-renderer/">Doxia Sitetools - Site | |
620 | * Renderer</a> for more information. | |
621 | * | |
622 | * @return Returns a {@link String} representing the name of current file of the (HTML) document being rendered, | |
623 | * relative to the site root. | |
624 | */ | |
625 | 108 | @Nonnull |
626 | public String getCurrentFileName() { | |
627 | 108 | return (String) velocityContext.get("currentFileName"); |
628 | } | |
629 | ||
630 | /** | |
631 | * @return Returns a {@link String} representing the location path of current rendered file. | |
632 | */ | |
633 | 96 | @Nonnull |
634 | public String getCurrentFileLocation() { | |
635 | 96 | final String projectSiteLoc = getProjectLocation(); |
636 | 96 | return URITool.toURI(projectSiteLoc).resolve(getCurrentFileName()).toString(); |
637 | } | |
638 | ||
639 | /** | |
640 | * {@inheritDoc} | |
641 | */ | |
642 | 6 | @Override |
643 | @SuppressWarnings("unchecked") | |
644 | @Nullable public <T> T eval(@Nullable final String vtl, @Nonnull final Class<T> requiredClass) { | |
645 | 6 | if (vtl == null) { |
646 | 0 | return null; |
647 | } | |
648 | 6 | final RenderTool renderTool = (RenderTool) getVelocityContext().get("render"); |
649 | 6 | try { |
650 | 6 | return (T) renderTool.eval(getVelocityContext(), vtl); |
651 | } catch (final Exception ex) { | |
652 | 0 | throw new RuntimeException("error when try evaluate '" + vtl + "'", ex); |
653 | } | |
654 | } | |
655 | ||
656 | /** | |
657 | * {@inheritDoc} | |
658 | */ | |
659 | 120 | @Override |
660 | public String relativeLink(final String href) { | |
661 | 120 | if (href == null) { |
662 | 0 | return null; |
663 | } | |
664 | 120 | if (isExternalLink(href)) { |
665 | 24 | return href; |
666 | } | |
667 | 96 | final String relativePath = (String) velocityContext.get("relativePath"); |
668 | 96 | String relativeLink = PathTool.calculateLink(href, relativePath); |
669 | 96 | relativeLink = relativeLink.replaceAll("\\\\", "/"); |
670 | 96 | if (Strings.isNullOrEmpty(relativeLink)) { |
671 | 0 | relativeLink = "./"; |
672 | } | |
673 | // Attempt to normalise the relative link - this is useful for active link | |
674 | // calculations and better relative links for subdirectories. | |
675 | // | |
676 | // The issue is particularly visible with pages in subdirectories, | |
677 | // so that if you are in <root>/dev/index.html, the relative menu link to | |
678 | // the _same_ page would likely be ../dev/index.html instead of '' or | |
679 | // 'index.html'. | |
680 | 96 | final String currentFileLoc = getCurrentFileLocation(); |
681 | 96 | final String absoluteLink = URITool.toURI(currentFileLoc).resolve(relativeLink).normalize().toString(); |
682 | 96 | if (currentFileLoc.equals(absoluteLink)) { |
683 | // for matching link, use empty relative link | |
684 | 0 | relativeLink = StringUtils.EMPTY; |
685 | } else { | |
686 | // relativize the absolute link based on current directory | |
687 | // (uses Maven project link relativization) | |
688 | 96 | final String currentFileDir = PathTool.getDirectoryComponent(currentFileLoc); |
689 | 96 | relativeLink = URITool.relativizeLink(currentFileDir, absoluteLink); |
690 | } | |
691 | 96 | if (LOGGER.isDebugEnabled()) { |
692 | 0 | LOGGER.debug("-- Relative Link ----------------------------------"); |
693 | 0 | LOGGER.debug("link: {}", href); |
694 | 0 | LOGGER.debug("currentFileLoc: {}", currentFileLoc); |
695 | 0 | LOGGER.debug("absoluteLink: {}", absoluteLink); |
696 | 0 | LOGGER.debug("relativeLink: {}", relativeLink); |
697 | 0 | LOGGER.debug("---------------------------------------------------"); |
698 | } | |
699 | 96 | return relativeLink; |
700 | } | |
701 | ||
702 | /** | |
703 | * @param url | |
704 | * a url. | |
705 | * @return Returns {@code true} whether the link is a external link to the site. | |
706 | */ | |
707 | 128 | public boolean isExternalLink(final String url) { |
708 | 128 | if (url == null) { |
709 | 1 | return false; |
710 | } | |
711 | 127 | final String absoluteResourceURL = this.value("absoluteResourceURL"); |
712 | 127 | if (!Strings.isNullOrEmpty(absoluteResourceURL) && url.startsWith(absoluteResourceURL)) { |
713 | 7 | return false; |
714 | } | |
715 | 120 | return url.toLowerCase().startsWith("http:/") || url.toLowerCase().startsWith("https:/") |
716 | || url.toLowerCase().startsWith("ftp:/") || url.toLowerCase().startsWith("mailto:") | |
717 | || url.toLowerCase().startsWith("file:/") || url.toLowerCase().indexOf("://") != -1; | |
718 | } | |
719 | ||
720 | /** | |
721 | * {@inheritDoc} | |
722 | */ | |
723 | 101 | @Override |
724 | public boolean isActiveLink(@Nullable final String href) { | |
725 | 101 | final String alignedFileName = (String) velocityContext.get("alignedFileName"); |
726 | 101 | if (href == null) { |
727 | 1 | return false; |
728 | } | |
729 | // either empty link (pointing to a page), or if the current file is index.html, | |
730 | // the link may point to the whole directory | |
731 | 100 | return Strings.isNullOrEmpty(href) || alignedFileName.endsWith("index.html") && ".".equals(href); |
732 | } | |
733 | ||
734 | /** | |
735 | * Converts a filename to pageId format. | |
736 | * | |
737 | * @param fileName | |
738 | * the filename to convert | |
739 | * @return Returns a {@link String} representing the pageId of {@code filename}. | |
740 | */ | |
741 | 30 | @Nullable public static String slugFilename(@Nullable final String fileName) { |
742 | 30 | if (fileName == null) { |
743 | 0 | return null; |
744 | } | |
745 | 30 | String currentFile = fileName; |
746 | ||
747 | // drop the extension | |
748 | 30 | final int lastDot = currentFile.lastIndexOf("."); |
749 | 30 | if (lastDot >= 0) { |
750 | 29 | currentFile = currentFile.substring(0, lastDot); |
751 | } | |
752 | ||
753 | // get the short ID (in case of nested files) | |
754 | // String fileName = new File(currentFile).getName(); | |
755 | // fileShortId = HtmlTool.slug(fileName); | |
756 | ||
757 | // full file ID includes the nested dirs | |
758 | // replace nesting "/" with "-" | |
759 | 30 | return HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-")); |
760 | } | |
761 | ||
762 | /** | |
763 | * @return Returns a {@link String} representing the relative path to root site. | |
764 | */ | |
765 | 6 | @Nonnull |
766 | public String getResourcePath() { | |
767 | 6 | final String absoluteResourceURL = this.value("absoluteResourceURL"); |
768 | 6 | String projectUrl = getProjectLocation(); |
769 | 6 | final String currentFileName = getCurrentFileName(); |
770 | 6 | if (!Strings.isNullOrEmpty(projectUrl) && currentFileName != null) { |
771 | 6 | if (projectUrl.charAt(projectUrl.length() - 1) != '/') { |
772 | 0 | projectUrl += '/'; |
773 | } | |
774 | 6 | final String currentFileDir = URITool.toURI(projectUrl).resolve(currentFileName).resolve(".").toString(); |
775 | 6 | return URITool.relativizeLink(currentFileDir, absoluteResourceURL); |
776 | } | |
777 | 0 | return (String) velocityContext.get("relativePath"); |
778 | } | |
779 | ||
780 | /** | |
781 | * Gets the reproduce build timestamp whether property 'project.build.outputTimestamp' is fill in. | |
782 | * | |
783 | * @return Returns a instance of {@code Date} representing the reproduce build timestamp whether property | |
784 | * 'project.build.outputTimestamp' is fill in. | |
785 | */ | |
786 | 0 | @Nullable public Date getBuildOutputTimestamp() throws ParseException { |
787 | 0 | if (!this.velocityContext.containsKey(PROJECT_BUILD_OUTPUTTIMESTAMP)) { |
788 | 0 | return null; |
789 | } | |
790 | 0 | Object outputTimestamp = this.velocityContext.get(PROJECT_BUILD_OUTPUTTIMESTAMP); |
791 | 0 | if (outputTimestamp != null) { |
792 | 0 | return ISO_8601BASIC_DATE.parse(outputTimestamp.toString()); |
793 | } | |
794 | 0 | return null; |
795 | } | |
796 | } |