1 /*
2 * Copyright 2012 Marek Romanowski
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 pl.matsuo.maven.skins.msb4;
17
18 import org.apache.maven.doxia.site.decoration.DecorationModel;
19 import org.apache.maven.project.MavenProject;
20 import org.apache.velocity.tools.ToolContext;
21 import org.apache.velocity.tools.config.DefaultKey;
22 import org.apache.velocity.tools.generic.SafeConfig;
23 import org.apache.velocity.tools.generic.ValueParser;
24 import org.codehaus.plexus.util.xml.Xpp3Dom;
25
26 /**
27 * An Apache Velocity tool that simplifies retrieval of custom configuration values for a
28 * Maven Site.
29 * <p>
30 * The tool is configured to access Maven site configuration of a skin inside {@code <custom>}
31 * element of site descriptor. It supports global properties (defined at skin level) and per-page
32 * properties (defined in {@code <page><mypage>} element). The per-page properties override the
33 * global ones.
34 * </p>
35 * <p>
36 * A sample configuration would be like that:
37 * </p>
38 *
39 * <pre>
40 * {@code
41 * <custom>
42 * <msb4Skin>
43 * <prop1>value1</prop1>
44 * <prop2>
45 * <prop21>value2</prop21>
46 * </prop2>
47 * <pages>
48 * <mypage project="myproject">
49 * <prop1>override value1</prop1>
50 * </mypage>
51 * </pages>
52 * </msb4Skin>
53 * </custom>
54 * }
55 * </pre>
56 * <p>
57 * To get the value of {@code prop1}, one would simply use {@code $config.prop1}. This would return
58 * "override value1". Then {@code $config.prop2} would return "value2" - the global value.
59 * </p>
60 * <p>
61 * The tool allows querying the value easily, falling back from page to global configuration to
62 * {@code null}, if none is available. It also provides convenience accessors for common values.
63 * </p>
64 * <p>
65 * Note
66 * </p>
67 *
68 * @author Andrius Velykis
69 * @since 1.0
70 */
71 @DefaultKey("config")
72 public class SkinConfigTool extends SafeConfig {
73
74 public static final String DEFAULT_KEY = "config";
75
76 /** By default use Msb4 skin configuration tag */
77 public static final String SKIN_KEY = "msb4Skin";
78
79 private String key = DEFAULT_KEY;
80 private String skinKey = SKIN_KEY;
81
82 /* Create dummy nodes to avoid null checks */
83 private Xpp3Dom globalProperties = new Xpp3Dom("");
84 private Xpp3Dom pageProperties = new Xpp3Dom("");
85 private String namespace = "";
86
87 private String projectId = null;
88 private String fileId = null;
89 // private String fileShortId = null;
90
91 /**
92 * {@inheritDoc}
93 *
94 * @see SafeConfig#configure(ValueParser)
95 */
96 @Override
97 protected void configure(ValueParser values) {
98 String altkey = values.getString("key");
99 if (altkey != null) {
100 setKey(altkey);
101 }
102
103 // allow changing skin key in the configuration
104 String altSkinKey = values.getString("skinKey");
105 if (altSkinKey != null) {
106 this.skinKey = altSkinKey;
107 }
108
109 // retrieve the decoration model from Velocity context
110 Object velocityContext = values.get("velocityContext");
111
112 if (!(velocityContext instanceof ToolContext)) {
113 return;
114 }
115
116 ToolContext ctxt = (ToolContext) velocityContext;
117
118 Object projectObj = ctxt.get("project");
119 if (projectObj instanceof MavenProject) {
120 MavenProject project = (MavenProject) projectObj;
121 String artifactId = project.getArtifactId();
122 // use artifactId "sluggified" as the projectId
123 projectId = HtmlTool.slug(artifactId);
124 }
125
126 // calculate the page ID from the current file name
127 Object currentFileObj = ctxt.get("currentFileName");
128 if (currentFileObj instanceof String) {
129
130 String currentFile = (String) currentFileObj;
131
132 // drop the extension
133 int lastDot = currentFile.lastIndexOf(".");
134 if (lastDot >= 0) {
135 currentFile = currentFile.substring(0, lastDot);
136 }
137
138 // get the short ID (in case of nested files)
139 // String fileName = new File(currentFile).getName();
140 // fileShortId = HtmlTool.slug(fileName);
141
142 // full file ID includes the nested dirs
143 // replace nesting "/" with "-"
144 fileId = HtmlTool.slug(currentFile.replace("/", "-").replace("\\", "-"));
145 }
146
147 Object decorationObj = ctxt.get("decoration");
148
149 if (!(decorationObj instanceof DecorationModel)) {
150 return;
151 }
152
153 DecorationModel decoration = (DecorationModel) decorationObj;
154 Object customObj = decoration.getCustom();
155
156 if (!(customObj instanceof Xpp3Dom)) {
157 return;
158 }
159
160 // Now that we have the custom node, get the global properties
161 // under the skin tag
162 Xpp3Dom customNode = (Xpp3Dom) customObj;
163 Xpp3Dom skinNode = customNode.getChild(skinKey);
164 String namespaceKey = ":" + skinKey;
165
166 if (skinNode == null) {
167 // try searching with any namespace
168 for (Xpp3Dom child : customNode.getChildren()) {
169 if (child.getName().endsWith(namespaceKey)) {
170 skinNode = child;
171 break;
172 }
173 }
174 }
175
176 if (skinNode != null) {
177 globalProperties = skinNode;
178
179 if (skinNode.getName().endsWith(namespaceKey)) {
180 // extract the namespace (including the colon)
181 namespace = skinNode.getName().substring(0, skinNode.getName().length() - namespaceKey.length() + 1);
182 }
183
184 // for page properties, retrieve the file name and drop the `.html`
185 // extension - this will be used, i.e. `index` instead of `index.html`
186 Xpp3Dom pagesNode = getChild(skinNode, "pages");
187 if (pagesNode != null) {
188
189 // Get the page for the file
190 // TODO try fileShortId as well?
191 Xpp3Dom page = getChild(pagesNode, fileId);
192
193 // Now check if the project artifact ID is set, and if so, if it matches the
194 // current project. This allows preventing accidental reuse of parent page
195 // configs in children modules
196 if (page != null && projectId != null) {
197 String pageProject = page.getAttribute("project");
198 if (pageProject != null && !projectId.equals(pageProject)) {
199 // project ID indicated, and is different - do not use the config
200 page = null;
201 }
202 }
203
204 if (page != null) {
205 pageProperties = page;
206 }
207 }
208 }
209 }
210
211 /**
212 * Retrieves the child node. Tests both default name and with namespace.
213 *
214 * @param parentNode
215 * @param name
216 * @return value
217 */
218 private Xpp3Dom getChild(Xpp3Dom parentNode, String name) {
219 Xpp3Dom child = parentNode.getChild(name);
220 if (child != null) {
221 return child;
222 }
223
224 return parentNode.getChild(namespace + name);
225 }
226
227 /**
228 * Sets the key under which this tool has been configured.
229 *
230 * @param key sets key
231 * @since 1.0
232 */
233 protected void setKey(String key) {
234 if (key == null) {
235 throw new NullPointerException("SkinConfigTool key cannot be null");
236 }
237 this.key = key;
238 }
239
240 /**
241 * Should return the key under which this tool has been configured. The default is `config`.
242 *
243 * @return key
244 * @since 1.0
245 */
246 public String getKey() {
247 return this.key;
248 }
249
250 /**
251 * Default accessor for config properties. Instead of using {@code $config.get("myproperty")},
252 * one can utilise Velocity fallback onto the default getter and use {@code $config.myproperty}.
253 *
254 * @param property
255 * the property of interest
256 * @return configuration node if found in the following sequence:
257 * <ol>
258 * <li>In page configuration</li>
259 * <li>In global configuration</li>
260 * <li>{@code null} otherwise</li>
261 * </ol>
262 * @since 1.0
263 */
264 public Xpp3Dom get(String property) {
265
266 // first try page properties
267 Xpp3Dom propNode = getChild(pageProperties, property);
268 if (propNode == null) {
269 // try global
270 propNode = getChild(globalProperties, property);
271 }
272
273 return propNode;
274 }
275
276 /**
277 * Retrieves the text value of the given {@code property}, e.g. as in
278 * {@code <myprop>value</myprop>}.
279 *
280 * @param property
281 * the property of interest
282 * @return the configuration value if found in page or globally, {@code null} otherwise.
283 * @see #get(String)
284 * @since 1.0
285 */
286 public String value(String property) {
287
288 Xpp3Dom propNode = get(property);
289
290 if (propNode == null) {
291 // not found
292 return null;
293 }
294
295 return propNode.getValue();
296 }
297
298 /**
299 * A convenience method to check if the value of the {@code property} is {@code "true"}.
300 *
301 * @param property
302 * the property of interest
303 * @return {@code true} if the configuration value is set either in page or globally, and is
304 * equal to {@code "true"}.
305 * @see #get(String)
306 * @since 1.0
307 */
308 public boolean is(String property) {
309 return "true".equals(value(property));
310 }
311
312 /**
313 * A convenience method to check if the value of the {@code property} is {@code "false"}. Useful
314 * for properties that are enabled by default - checks if the property is set to {@code "false"}
315 * explicitly.
316 *
317 * @param property
318 * the property of interest
319 * @return {@code true} if the configuration value is set either in page or globally, and is
320 * equal to {@code "false"}. Note that this will return {@code false} if property is not
321 * set at all.
322 * @see #get(String)
323 * @since 1.0
324 */
325 public boolean not(String property) {
326 return "false".equals(value(property));
327 }
328
329 /**
330 * A convenience method to check if the {@code property} is set to a specific value.
331 *
332 * @param property
333 * the property of interest
334 * @param value
335 * the property value to check
336 * @return {@code true} if the configuration value is set either in page or globally, and is
337 * equal to {@code value}.
338 * @see #get(String)
339 * @since 1.0
340 */
341 public boolean isValue(String property, String value) {
342 return value != null && value.equals(value(property));
343 }
344
345 public String getProjectId() {
346 return projectId;
347 }
348
349 public String getFileId() {
350 return fileId;
351 }
352
353 }