View Javadoc
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.msb3;
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   *   <msb3Skin>
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   *   </msb3Skin>
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 Msb3 skin configuration tag */
77  	public static final String SKIN_KEY = "msb3Skin";
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
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 	 * @since 1.0
231 	 */
232 	protected void setKey(String key) {
233 		if (key == null) {
234 			throw new NullPointerException("SkinConfigTool key cannot be null");
235 		}
236 		this.key = key;
237 	}
238 
239 	/**
240 	 * Should return the key under which this tool has been configured. The default is `config`.
241 	 * 
242 	 * @since 1.0
243 	 */
244 	public String getKey() {
245 		return this.key;
246 	}
247 
248 	/**
249 	 * Default accessor for config properties. Instead of using {@code $config.get("myproperty")},
250 	 * one can utilise Velocity fallback onto the default getter and use {@code $config.myproperty}.
251 	 * 
252 	 * @param property
253 	 *            the property of interest
254 	 * @return configuration node if found in the following sequence:
255 	 *         <ol>
256 	 *         <li>In page configuration</li>
257 	 *         <li>In global configuration</li>
258 	 *         <li>{@code null} otherwise</li>
259 	 *         </ol>
260 	 * @since 1.0
261 	 */
262 	public Xpp3Dom get(String property) {
263 
264 		// first try page properties
265 		Xpp3Dom propNode = getChild(pageProperties, property);
266 		if (propNode == null) {
267 			// try global
268 			propNode = getChild(globalProperties, property);
269 		}
270 
271 		return propNode;
272 	}
273 
274 	/**
275 	 * Retrieves the text value of the given {@code property}, e.g. as in
276 	 * {@code <myprop>value</myprop>}.
277 	 * 
278 	 * @param property
279 	 *            the property of interest
280 	 * @return the configuration value if found in page or globally, {@code null} otherwise.
281 	 * @see #get(String)
282 	 * @since 1.0
283 	 */
284 	public String value(String property) {
285 
286 		Xpp3Dom propNode = get(property);
287 
288 		if (propNode == null) {
289 			// not found
290 			return null;
291 		}
292 
293 		return propNode.getValue();
294 	}
295 
296 	/**
297 	 * A convenience method to check if the value of the {@code property} is {@code "true"}.
298 	 * 
299 	 * @param property
300 	 *            the property of interest
301 	 * @return {@code true} if the configuration value is set either in page or globally, and is
302 	 *         equal to {@code "true"}.
303 	 * @see #get(String)
304 	 * @since 1.0
305 	 */
306 	public boolean is(String property) {
307 		return "true".equals(value(property));
308 	}
309 
310 	/**
311 	 * A convenience method to check if the value of the {@code property} is {@code "false"}. Useful
312 	 * for properties that are enabled by default - checks if the property is set to {@code "false"}
313 	 * explicitly.
314 	 * 
315 	 * @param property
316 	 *            the property of interest
317 	 * @return {@code true} if the configuration value is set either in page or globally, and is
318 	 *         equal to {@code "false"}. Note that this will return {@code false} if property is not
319 	 *         set at all.
320 	 * @see #get(String)
321 	 * @since 1.0
322 	 */
323 	public boolean not(String property) {
324 		return "false".equals(value(property));
325 	}
326 
327 	/**
328 	 * A convenience method to check if the {@code property} is set to a specific value.
329 	 * 
330 	 * @param property
331 	 *            the property of interest
332 	 * @param value
333 	 *            the property value to check
334 	 * @return {@code true} if the configuration value is set either in page or globally, and is
335 	 *         equal to {@code value}.
336 	 * @see #get(String)
337 	 * @since 1.0
338 	 */
339 	public boolean isValue(String property, String value) {
340 		return value != null && value.equals(value(property));
341 	}
342 	
343 	public String getProjectId() {
344 		return projectId;
345 	}
346 	
347 	public String getFileId() {
348 		return fileId;
349 	}
350 
351 }