View Javadoc

1   /*
2      Copyright 2009 jtpl.sourceforge.net
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 net.sourceforge.jtpl;
17  
18  import java.io.File;
19  import java.io.FileReader;
20  import java.io.IOException;
21  import java.io.Reader;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.Set;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  /**
30   * <b>Jtpl: a very simple template engine for Java</b><br>
31   * Contact: <a href="mailto:emmanuel.alliel@gmail.com">emmanuel.alliel@gmail.com</a><br>
32   * Web: <a href="http://jtpl.sourceforge.net">http://jtpl.sourceforge.net</a><br>
33   * 
34   * @version $LastChangedRevision: 51 $
35   * @author Emmanuel ALLIEL
36   * @author Staffan Olsson
37   * 
38   * <p>
39   * Template syntax:<br>
40   * &nbsp;&nbsp;&nbsp;Variables:<br>
41   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>{VARIABLE_NAME}</code><br>
42   * &nbsp;&nbsp;&nbsp;Blocks:<br>
43   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>&lt;!-- BEGIN: BlockName --&gt;</code><br>
44   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>&lt;!-- BEGIN: SubBlockName --&gt;</code><br>
45   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>&lt;!-- END: SubBlockName --&gt;</code><br>
46   * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<code>&lt;!-- END: BlockName --&gt;</code><br>
47   * <p>
48   * License: Apache 2.0<br>
49  */
50  
51  public class Jtpl 
52  {
53  	private HashMap blocks = new HashMap();
54  	private HashMap parsedBlocks = new HashMap();
55  	private HashMap subBlocks = new HashMap();
56  	private HashMap vars = new HashMap();
57  	// flag for backwards compatibility, will probably be reomved in future releases
58  	private boolean failSilently = false;
59  	private boolean implicitMain = false;
60  	
61  	/**
62  	* Constructs a Jtpl object and reads the template from a file.
63  	* For backwards compatibility this constructor enables 'failSilently'.
64  	* @param fileName the <code>file name</code> of the template, exemple: "java/folder/index.tpl"
65  	* @throws IOException when an i/o error occurs while reading the template.
66  	*/
67  	public Jtpl(String fileName) throws IOException
68  	{
69  		this(new File(fileName));
70  		// this is the old constructor so it enables the old (pre-1.3) behavior on errors
71  		this.failSilently = true;
72  	}
73  
74  	/**
75  	* Constructs a Jtpl object and reads the template from a file.
76  	* @param file readable file containing template source
77  	* @throws IOException when an i/o error occurs while reading the template.
78  	*/
79  	public Jtpl(File file) throws IOException
80  	{
81  		FileReader fr = new FileReader(file);
82  		String fileText = readFile(fr);
83  		makeTree(fileText);
84  	}	
85  	
86  	/**
87  	* Constructs a Jtpl object and reads the template from arbitrary input source.
88  	* @param template the template source
89  	* @throws IOException when an i/o error occurs while reading the template.
90  	*/	
91  	public Jtpl(Reader template) throws IOException
92  	{
93  		String fileText = readFile(template);
94  		makeTree(fileText);
95  	}
96  	
97  	/**
98  	* Assign a template variable.
99  	* For variables that are used in blocks, the variable value
100 	* must be set before <code>parse</code> is called.
101 	* @param varName the name of the variable to be set.
102 	* @param varData the new value of the variable.
103 	*/
104 	public void assign(String varName, String varData)
105 	{
106 		vars.put(varName, varData);
107 	}
108 	
109 	/**
110 	* Generates the HTML page and return it into a String.
111 	*/
112 	public String out()
113 	{
114 		if (this.implicitMain) {
115 			this.parse("main");
116 		}
117 		Object main = parsedBlocks.get("main");
118 		if (main == null) {
119 			throw new IllegalStateException("'main' block not parsed");
120 		}
121 		return(main.toString());
122 	}
123 	
124 	/**
125 	* Parse a template block.
126 	* If the block contains variables, these variables must be set
127 	* before the block is added.
128 	* If the block contains subblocks, the subblocks
129 	* must be parsed before this block.
130 	* @param blockName the name of the block to be parsed.
131 	* @throws IllegalArgumentException if the block name is not found (and failSiletly==false)
132 	*/
133 	public void parse(String blockName) throws IllegalArgumentException
134 	{
135 		String copy = "";
136 		if (implicitMain && !"main".equals(blockName) && !blockName.startsWith("main.")) {
137 			blockName = "main." + blockName;
138 		}
139 		try {
140 			copy = blocks.get(blockName).toString();
141 		} catch (NullPointerException e) {
142 			if (!this.failSilently) throw new IllegalArgumentException(
143 					"Block '" + blockName + "' not found." +
144 							" Matches " + locateBlock(blockName));
145 		}
146 		Pattern pattern = Pattern.compile("\\{([\\w\\.]+)\\}");
147 		Matcher matcher = pattern.matcher(copy);
148 		pattern = Pattern.compile("_BLOCK_\\.(.+)");
149 		for (Matcher matcher2; matcher.find();)
150 		{
151 			String match = matcher.group(1);
152 			matcher2 = pattern.matcher(match);
153 			if (matcher2.find())
154 			{
155 				if (parsedBlocks.containsKey(matcher2.group(1)))
156 				{
157 					copy = copy.replaceFirst("\\{"+match+"\\}", escape(
158 							parsedBlocks.get(matcher2.group(1)).toString()));
159 				}
160 				else
161 				{
162 					copy = copy.replaceFirst("\\{"+match+"\\}", "");
163 				}
164 			}
165 			else
166 			{
167 				if (vars.containsKey(match))
168 				{
169 					copy = copy.replaceFirst("\\{"+match+"\\}", escape(
170 							vars.get(match).toString()));
171 				}
172 				else
173 				{
174 					// Leave unchanged because it might be wanted in output.
175 					// Can always be removed by assigning empty value.					
176 					//copy = copy.replaceFirst("\\{"+match+"\\}", "");
177 				}
178 			}
179 		}
180 		if (parsedBlocks.containsKey(blockName))
181 		{
182 			parsedBlocks.put(blockName, parsedBlocks.get(blockName) + copy);
183 		}
184 		else
185 		{
186 			parsedBlocks.put(blockName, copy);
187 		}
188 		if (subBlocks.containsKey(blockName))
189 		{
190 			parsedBlocks.put(subBlocks.get(blockName), "");
191 		}
192 	}
193 	
194 	/**
195 	 * Template parsing uses regex replace to insert result text,
196 	 * which means that special characters in replacement string must be escaped.
197 	 * @param replacement The text that should appear in output.
198 	 * @return Text escaped so that it works as String.replaceFirst replacement.
199 	 */
200 	protected String escape(String replacement) {
201 		return replacement.replace("\\", "\\\\").replace("$", "\\$");
202 	}
203 	
204 	/**
205 	 * Lists the blocks that end with the given blockName.
206 	 * @param blockName The name as used in parse
207 	 * @return Blocks where blockName is the child
208 	 *  (the Set's toString lists the full names)
209 	 */
210 	protected Set locateBlock(final String blockName) {
211 		Set matches = new java.util.HashSet();
212 		for (Iterator it = blocks.keySet().iterator(); it.hasNext(); ) {
213 			Object b = it.next();
214 			if (b.toString().endsWith('.' + blockName)) matches.add(b);
215 		}
216 		return matches;
217 	}
218 	
219 	private String readFile(Reader fr) throws IOException
220 	{
221 		StringBuffer content = new StringBuffer();
222 		for (int c; (c = fr.read()) != -1; content.append((char)c));
223 		fr.close();
224 		return content.toString();
225 	}
226 	
227 	private void makeTree(String fileText)
228 	{
229 		// BEGIN: implicit main
230 		if (!Pattern.compile(".*<!--\\s*BEGIN\\s*:\\s*main\\s*-->.*", Pattern.DOTALL)
231 				.matcher(fileText).matches()) {
232 			this.implicitMain  = true; // affects parse(block) and out()
233 			fileText = "<!-- BEGIN: main -->" + fileText + "<!-- END: main -->";
234 		}
235 		// END: implicit main
236 		Pattern pattern = Pattern.compile("<!--\\s*(BEGIN|END)\\s*:\\s*(\\w+)\\s*-->(.*?)(?=(?:<!--\\s*(?:BEGIN|END)\\s*:\\s*\\w+\\s*-->)|(?:\\s*$))", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
237 		Matcher matcher = pattern.matcher(fileText);
238 		ArrayList blockNames = new ArrayList();
239 		String parentName = "";
240 		int lastlength = 0; // used in newline trimming to see if one block immediately follows the previous
241 		while (matcher.find())
242 		{
243 			// BEGIN: newline trimming
244 			String after = matcher.group(3); // contents after tag
245 			if (lastlength == 0 || fileText.charAt(matcher.start() - 1) == '\n') {
246 				after = after.replaceFirst("^\\r?\\n", "");
247 			}
248 			lastlength = after.length();
249 			// END: newline trimming
250 			if (matcher.group(1).toUpperCase().equals("BEGIN"))
251 			{
252 				parentName = implode(blockNames);
253 				blockNames.add(matcher.group(2));
254 				String currentBlockName = implode(blockNames);
255 				if (blocks.containsKey(currentBlockName))
256 				{
257 					blocks.put(currentBlockName, blocks.get(currentBlockName) + after);
258 				}
259 				else
260 				{
261 					blocks.put(currentBlockName, after);
262 				}
263 				if (blocks.containsKey(parentName))
264 				{
265 					blocks.put(parentName, blocks.get(parentName) + "{_BLOCK_." + currentBlockName + "}");
266 				}
267 				else
268 				{
269 					blocks.put(parentName, "{_BLOCK_." + currentBlockName + "}");
270 				}
271 				subBlocks.put(parentName, currentBlockName);
272 				subBlocks.put(currentBlockName, "");
273 			}
274 			else if (matcher.group(1).toUpperCase().equals("END"))
275 			{
276 				blockNames.remove(blockNames.size()-1);
277 				parentName = implode(blockNames);
278 				if (blocks.containsKey(parentName))
279 				{
280 					blocks.put(parentName, blocks.get(parentName) + after);
281 				}
282 				else
283 				{
284 					blocks.put(parentName, after);
285 				}
286 			}
287 		}
288 	}
289 	
290 	private String implode(ArrayList al)
291 	{
292 		String ret = "";
293 		for (int i = 0; al.size() > i; i++)
294 		{
295 			if (i != 0)
296 			{
297 				ret += ".";
298 			}
299 			ret += al.get(i);
300 		}
301 		return (ret);
302 	}
303 	
304 }