1 | /* |
2 | * $Id: Template.java,v 1.4 2004/11/04 19:27:37 hastings Exp $ |
3 | * |
4 | * Copyright (c) 2004, Moebius Solutions, Inc. |
5 | * All rights reserved. |
6 | * |
7 | * Redistribution and use in source and binary forms, with or without |
8 | * modification, are permitted provided that the following conditions |
9 | * are met: |
10 | * |
11 | * Redistributions of source code must retain the above copyright |
12 | * notice, this list of conditions and the following disclaimer. |
13 | * |
14 | * Redistributions in binary form must reproduce the above |
15 | * copyright notice, this list of conditions and the following |
16 | * disclaimer in the documentation and/or other materials provided |
17 | * with the distribution. |
18 | * |
19 | * Neither the name of Moebius Solutions, Inc. nor the names of |
20 | * its contributors may be used to endorse or promote products |
21 | * derived from this software without specific prior written |
22 | * permission. |
23 | * |
24 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
25 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
26 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
27 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
28 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
29 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
30 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
31 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
32 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
33 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
34 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED |
35 | * OF THE POSSIBILITY OF SUCH DAMAGE. |
36 | */ |
37 | |
38 | package com.moesol.generator.core; |
39 | |
40 | import java.io.*; |
41 | import java.util.*; |
42 | import java.lang.reflect.*; |
43 | |
44 | |
45 | interface NameParsedListener { |
46 | public String nameParsed(String name) throws ApplyException; |
47 | } |
48 | |
49 | /** |
50 | * Process a template file and replace occurances of ${foo} with the |
51 | * value of the property foo. |
52 | */ |
53 | public class Template { |
54 | public Template(InputStream is) { |
55 | in = new LineNumberReader(new InputStreamReader(is)); |
56 | } |
57 | public Template(Reader r) { |
58 | in = new LineNumberReader(r); |
59 | } |
60 | public Template(String t) { |
61 | in = new LineNumberReader(new StringReader(t)); |
62 | } |
63 | public void setInput(Reader r) { |
64 | in = new LineNumberReader(r); |
65 | } |
66 | /** |
67 | * @return the input reader used for the source of the template. |
68 | * Note that input reader set by the constructor and setInput is |
69 | * wrapped by a LineNumberReader, which this method returns. |
70 | */ |
71 | public Reader getInput() { |
72 | return in; |
73 | } |
74 | public void setObject(Object o) { |
75 | object = o; |
76 | } |
77 | public Object getObject() { |
78 | return object; |
79 | } |
80 | /** |
81 | * Message to prepend to thrown error messages. |
82 | */ |
83 | public void setErrorPrefix(String info) { |
84 | error_info = info; |
85 | } |
86 | public String getErrorPrefix() { |
87 | return error_info; |
88 | } |
89 | public void apply(Properties p, Writer out) |
90 | throws ApplyException |
91 | { |
92 | try { |
93 | int c; |
94 | while ((c = in.read()) != -1) { |
95 | if (c == '$') { |
96 | replaceName(p, out, '$'); |
97 | } else if (c == '#') { |
98 | callName(p, out); |
99 | } else if (c == '\\') { |
100 | escapeSeen(out); |
101 | } else if (c == '"') { |
102 | writeChar(out, c); |
103 | // Do not treat quotes specially. |
104 | // Replace ${xx} inside of quotes. |
105 | // quoteSeen(out); |
106 | } else { |
107 | writeChar(out, c); |
108 | } |
109 | } |
110 | } catch (IOException e) { |
111 | throw new ApplyException(error_info + "io error at: " + in.getLineNumber(), e); |
112 | } |
113 | } |
114 | |
115 | private void replaceName(final Properties p, Writer out, char cause) |
116 | throws ApplyException, IOException |
117 | { |
118 | NameParsedListener l = new NameParsedListener() { |
119 | public String nameParsed(String name) throws ApplyException { |
120 | String replace = p.getProperty(name.toString()); |
121 | if (null == replace) { |
122 | throw new ApplyException(error_info + "line: " |
123 | + in.getLineNumber() |
124 | + ": No property for ${"+name+"}"); |
125 | } |
126 | return replace; |
127 | } |
128 | }; |
129 | String replacement = needOpenBrace(p, l, cause); |
130 | |
131 | Template t = new Template(new StringReader(replacement)); |
132 | t.setObject(t.getObject()); |
133 | t.apply(p, out); |
134 | } |
135 | private void callName(final Properties p, final Writer out) |
136 | throws ApplyException, IOException |
137 | { |
138 | NameParsedListener l = new NameParsedListener() { |
139 | public String nameParsed(String name) throws ApplyException { |
140 | // TODO better arg parsing |
141 | int left = name.indexOf('('); |
142 | int right = name.lastIndexOf(')'); |
143 | if (left > 0 && right > left) { |
144 | callWithArg(name.substring(0,left), name.substring(left+1, right)); |
145 | } else { |
146 | callSimple(name); |
147 | } |
148 | return ""; |
149 | } |
150 | private void callSimple(String name) throws ApplyException { |
151 | Class c = object.getClass(); |
152 | Class param_types[] = { Writer.class }; |
153 | Object params[] = { out }; |
154 | try { |
155 | Method m = c.getMethod(name, param_types); |
156 | m.invoke(object, params); |
157 | } catch (InvocationTargetException e) { |
158 | throwUnwrappedException(e); |
159 | throw new ApplyException(error_info + "error: " + in.getLineNumber(), e); |
160 | } catch (Exception e) { |
161 | throw new ApplyException(error_info + "error: " + in.getLineNumber(), e); |
162 | } |
163 | } |
164 | private void callWithArg(String name, String arg) throws ApplyException { |
165 | Class c = object.getClass(); |
166 | Class param_types[] = { Writer.class, String.class }; |
167 | Object params[] = { out, arg }; |
168 | try { |
169 | Method m = c.getMethod(name, param_types); |
170 | m.invoke(object, params); |
171 | } catch (InvocationTargetException e) { |
172 | throwUnwrappedException(e); |
173 | throw new ApplyException(error_info + "error: " + in.getLineNumber(), e); |
174 | } catch (Exception e) { |
175 | throw new ApplyException(error_info + "error: " + in.getLineNumber(), e); |
176 | } |
177 | } |
178 | }; |
179 | needOpenBrace(p, l, '#'); |
180 | } |
181 | private void throwUnwrappedException(InvocationTargetException e) |
182 | throws ApplyException |
183 | { |
184 | if (e.getCause() != null) { |
185 | if (e.getCause() instanceof ApplyException) { |
186 | throw (ApplyException)e.getCause(); |
187 | } |
188 | } |
189 | } |
190 | private void escapeSeen(Writer out) throws IOException { |
191 | int c; |
192 | if ((c = in.read()) == -1) { |
193 | return; |
194 | } |
195 | writeChar(out, c); |
196 | } |
197 | private void quoteSeen(Writer out) throws IOException { |
198 | int c; |
199 | while ((c = in.read()) != -1) { |
200 | if (c == '\\') { |
201 | escapeSeen(out); |
202 | } else if (c == '"') { |
203 | writeChar(out, c); |
204 | return; |
205 | } else { |
206 | writeChar(out, c); |
207 | } |
208 | } |
209 | } |
210 | |
211 | private String needOpenBrace(Properties p, NameParsedListener l, char cause) |
212 | throws ApplyException, IOException |
213 | { |
214 | int c; |
215 | if ((c = in.read()) == -1) { |
216 | // Caused by "foo$" or "bar#" input escape the cause and |
217 | // return |
218 | return "\\" + cause; |
219 | } |
220 | if (c == '{') { |
221 | return scanName(p, l); |
222 | } |
223 | // Caused by "foo$bar" input, just give up on the |
224 | // replacement and return the escaped cause plus bad char. |
225 | return "\\" + cause + (char)c; |
226 | } |
227 | private String scanName(Properties p, NameParsedListener l) |
228 | throws IOException, ApplyException |
229 | { |
230 | StringBuffer name = new StringBuffer(); |
231 | |
232 | int c; |
233 | while ((c = in.read()) != -1) { |
234 | if (c == '}') { |
235 | return l.nameParsed(name.toString()); |
236 | } else if (c == '$') { |
237 | name.append(needOpenBrace(p, l, '$')); |
238 | } else if (c == '\\') { |
239 | StringWriter sw = new StringWriter(1); |
240 | escapeSeen(sw); |
241 | name.append(sw.getBuffer()); |
242 | } else if (c == '#') { |
243 | throw new ApplyException(error_info + "parse error at: " |
244 | + in.getLineNumber() |
245 | + ", nested calls not allowed in ${" |
246 | + name.toString()); |
247 | } else if (c == '\n') { |
248 | throw new ApplyException(error_info + "parse error at: " |
249 | + in.getLineNumber() |
250 | + ", newline embedded in ${" |
251 | + name.toString()); |
252 | } else { |
253 | name.append((char)c); |
254 | } |
255 | } |
256 | throw new ApplyException("at: " + in.getLineNumber() + " EOF looking for } "); |
257 | } |
258 | |
259 | /** |
260 | * Write c to out, but skipping CR characters. |
261 | */ |
262 | private void writeChar(Writer out, int c) throws IOException { |
263 | if (c == '\r') { |
264 | return; |
265 | } |
266 | out.write(c); |
267 | } |
268 | |
269 | private String error_info = ""; |
270 | private LineNumberReader in; |
271 | private Object object; |
272 | } |