001: /*
002: * Copyright (c) 2004-2006, Jean-François Brazeau. All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * 2. Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: *
014: * 3. The name of the author may not be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
018: * IMPLIEDWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
019: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
020: * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
021: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
022: * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
023: * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
024: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
025: * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
026: * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027: */
028: package jfb.tools.activitymgr.report;
029:
030: import java.util.Collection;
031: import java.util.Map;
032: import java.util.Properties;
033: import java.util.Set;
034: import java.util.Stack;
035: import java.util.regex.Matcher;
036: import java.util.regex.Pattern;
037:
038: import org.apache.log4j.Logger;
039:
040: /**
041: * Classe offrant des services d'accès aux propriétés de configuration
042: * pour la génération de rapports avec Velocity.
043: *
044: * <p>
045: * Pour Velocity, une variable de type <code>${a.b}</code> peut vouloir
046: * dire au choix :
047: * <ul>
048: * <li>l'attibut b de l'instance a (ou a est un bean Java)</li>
049: * <li>la propriété b de l'instance a (ou a est une Map)</li>
050: * </ul>
051: * </p>
052: *
053: * <p>
054: * On exploite la deuxième possibilité pour rendre '<i>visible</i>' une
055: * propriété définie dans le fichier de configuration des rapports dans
056: * un template velocity. Une instance de <code>PropertiesHelper</code>
057: * est enregistrée dans le contexte velocity sous le nom <code>props</code>.
058: * Comme cette classe implémente l'interface <code>java.util.Map</code>,
059: * Velocity invoque la méthode <code>get(String)</code> en lui passant en
060: * argument la chaine aussitot placée après <code>props.</code>.
061: * La fin de la propriété est marquée par l'invocation de la méthode
062: * <code>get()</code>.
063: * </p>
064: *
065: * <p>
066: * Il est supposé que les propriétés contenues dans le fichier de configuration
067: * et accédées depuis les templates Velocity sont préfixées par :<br>
068: * <code>reports.<reportId></code> ou <code>reportId</code> désigne
069: * l'identifiant du rapport.
070: * </p>
071: *
072: * <p>
073: * Exemple : <code>${props.startYear.get()}</code> pour le rapport dont
074: * l'identifiant est <code>myReport</code> va référencer la propriété
075: * <code>reports.myReport.startYear</code> dans le fichier de configuration.
076: * Si cette propriété n'est pas définie, le moteur regarde ensuite
077: * tour à tour les propriétés <code>reports.startYear</code> et
078: * <code>startYear</code>. Si toutes ces propriétés sont indéfinies, une
079: * exception est levée.
080: * </p>
081: *
082: * <p>
083: * Dans le cas ou le nom de la propriété est composée
084: * (ex : <code>start.year</code>), la méthode <code>get</code> retourne
085: * l'instance de <code>PropertiesHelper</code> et la première partie du nom de
086: * la propriété est déposée sur la pile. Lors de l'invocation suivante, les
087: * différentes parties du nom de la propriété sont réassemblées pour former
088: * le nom de la propriété dans son ensemble.
089: * </p>
090: *
091: * <p>Pour cette raison, cette classe n'est pas thread safe.</p>
092: *
093: * @see jfb.tools.activitymgr.report.ReportMgr
094: */
095: public class PropertiesHelper implements Map {
096:
097: /** Logger */
098: private static Logger log = Logger
099: .getLogger(PropertiesHelper.class);
100:
101: /** Dictionnaire de propriétés */
102: private Properties props;
103:
104: /** Identifiant courant de rapport */
105: private String currentReportId;
106:
107: /** Pile utilisée pour extraire les propriétés */
108: private Stack stack = new Stack();
109:
110: /**
111: * Constructeur par défaut.
112: * @param reportId identifiant du rapport courant.
113: * @param props dictionnaire par défaut.
114: */
115: public PropertiesHelper(String reportId, Properties props) {
116: this .currentReportId = reportId;
117: this .props = props;
118: }
119:
120: /**
121: * Retourne la valeur de la propriété dont la clé est constitué
122: * des chaînes empilées dans la pile.
123: * @return la valeur de la propriété.
124: * @throws ReportException levé dans le cas où la clé n'existe pas.
125: */
126: public String get() throws ReportException {
127: // Construction de la fin de la clé
128: StringBuffer buf = new StringBuffer();
129: for (int i = 0; i < stack.size(); i++) {
130: if (i > 0)
131: buf.append('.');
132: buf.append(stack.get(i));
133: }
134: log.debug("get(" + buf.toString() + ")");
135: // Purge de la pile
136: stack.clear();
137: // Lecture de la 1° clé par défaut
138: String propKey0 = new StringBuffer("reports.").append(
139: currentReportId).append('.').append(buf.toString())
140: .toString();
141: log.debug("PropertyKey=" + propKey0);
142: String propValue = props.getProperty(propKey0);
143: // Lecture de la 2° clé par défaut si la 1° n'a rien donné
144: if (propValue == null) {
145: String propKey1 = new StringBuffer("reports.").append(
146: buf.toString()).toString();
147: log.debug("PropertyKey=" + propKey1);
148: propValue = props.getProperty(propKey1);
149: // Lecture de la 3° clé par défaut si la 1° et la 2°
150: // n'ont rien donné
151: if (propValue == null) {
152: String propKey2 = buf.toString();
153: log.debug("PropertyKey=" + propKey2);
154: propValue = props.getProperty(propKey2);
155: if (propValue == null)
156: throw new ReportException("Parameter '"
157: + buf.toString()
158: + "' is not specified (under '" + propKey0
159: + "', '" + propKey1 + "' or '" + propKey2
160: + "'", null);
161: }
162: }
163: // Retour du résultat
164: String value = substitueVariables(propValue);
165: log.debug("PropertyValue=" + value);
166: return value;
167: }
168:
169: /**
170: * Retourne la valeur de la propriété dont le postfix est spécifié.
171: * @param postPropertyKey le postfix de clé de propriété.
172: * @return la valeur de la propriété.
173: * @throws ReportException levé en cas de non conformité de la valeur
174: * de la chaîne.
175: */
176: public String getProperty(String postPropertyKey)
177: throws ReportException {
178: String propKey0 = new StringBuffer("reports.").append(
179: currentReportId).append('.').append(postPropertyKey)
180: .toString();
181: log.debug("PropertyKey=" + propKey0);
182: String propValue = props.getProperty(propKey0);
183: if (propValue != null)
184: propValue = substitueVariables(propValue);
185: log.debug("PropertyValue=" + propValue);
186: return propValue;
187: }
188:
189: /**
190: * Substitue les chaînes de type ${xxx.xxx.xxx} par leur valeur
191: * trouvées dans les propriétés système.
192: * @param s la chaîne de caractère à parcourir.
193: * @return le résultat.
194: * @throws ReportException levé en cas de non conformité de la chaîne.
195: */
196: private String substitueVariables(String s) throws ReportException {
197: return substitueVariables(new Stack(), s);
198: }
199:
200: /* (non-Javadoc)
201: * @see java.util.Map#get(java.lang.Object)
202: */
203: public Object get(Object key) {
204: log.debug("Append '" + key + "' to property stack");
205: stack.push(key);
206: return this ;
207: }
208:
209: /**
210: * Remplace les occurences de variables de type <code>${xxx}</code> par
211: * leur valeur lorsqu'elle existe dans le dictionnaire.
212: * @param stack pile permettant de détecter les références circulaires.
213: * @param s la chaîne contenant les variables.
214: * @return la chaîne substituée.
215: * @throws ReportException levé en cas de détection de références
216: * circulaires.
217: */
218: private String substitueVariables(Stack stack, String s)
219: throws ReportException {
220: String result = null;
221: if (s != null) {
222: // Préparation de l'expression régulière
223: Pattern pattern = Pattern
224: .compile("\\$\\{[a-zA-Z0-9.'_-]+\\}");
225: Matcher matcher = pattern.matcher(s);
226: // Préparation du buffer accueillant la chaine substituée
227: StringBuffer buf = new StringBuffer();
228: int idx = 0;
229: // Parcours des valeurs obtenues
230: while (matcher.find()) {
231: // Récupération de la chaine ${xxx.xxx.xxx}
232: String matched = matcher.group();
233: // Ajout du bout de chaine précédent la valeur trouvée
234: buf.append(s.substring(idx, (idx = s.indexOf(matched,
235: idx))));
236: // Extraction du nom de la variable
237: String name = matched
238: .substring(2, matched.length() - 1);
239: // Si cette variable est dans la pile => référence cyclique
240: if (stack.contains(name))
241: throw new ReportException("Circular reference for"
242: + " property '${" + name + "}'", null);
243: // Récupération de sa valeur, et ajout au buffer
244: String value = props.getProperty(name);
245: if (value == null)
246: value = System.getProperty(name);
247: // Si la valeur est non nulle, on effectue le traitement
248: // récursivement
249: if (value != null) {
250: stack.push(name);
251: value = substitueVariables(stack, value);
252: stack.pop();
253: }
254: //if (value==null)
255: // throw new ReportException("Missing property " +
256: // "variable '${" + name + "}'", null);
257: log.debug("Var(" + name + ")='" + value + "'");
258: // Concaténation...
259: buf.append(value != null ? value : matched);
260: // Incrémentation de l'index
261: idx += matched.length();
262: }
263: // Ajout de la fin de la chaine
264: buf.append(s.substring(idx));
265: result = buf.toString();
266: }
267: // Retour du résultat
268: return result;
269: }
270:
271: /* (non-Javadoc)
272: * @see java.util.Map#size()
273: */
274: public int size() {
275: throw new Error("Not implemented");
276: }
277:
278: /* (non-Javadoc)
279: * @see java.util.Map#clear()
280: */
281: public void clear() {
282: throw new Error("Not implemented");
283: }
284:
285: /* (non-Javadoc)
286: * @see java.util.Map#isEmpty()
287: */
288: public boolean isEmpty() {
289: throw new Error("Not implemented");
290: }
291:
292: /* (non-Javadoc)
293: * @see java.util.Map#containsKey(java.lang.Object)
294: */
295: public boolean containsKey(Object key) {
296: throw new Error("Not implemented");
297: }
298:
299: /* (non-Javadoc)
300: * @see java.util.Map#containsValue(java.lang.Object)
301: */
302: public boolean containsValue(Object value) {
303: throw new Error("Not implemented");
304: }
305:
306: /* (non-Javadoc)
307: * @see java.util.Map#values()
308: */
309: public Collection values() {
310: throw new Error("Not implemented");
311: }
312:
313: /* (non-Javadoc)
314: * @see java.util.Map#putAll(java.util.Map)
315: */
316: public void putAll(Map t) {
317: throw new Error("Not implemented");
318: }
319:
320: /* (non-Javadoc)
321: * @see java.util.Map#entrySet()
322: */
323: public Set entrySet() {
324: throw new Error("Not implemented");
325: }
326:
327: /* (non-Javadoc)
328: * @see java.util.Map#keySet()
329: */
330: public Set keySet() {
331: throw new Error("Not implemented");
332: }
333:
334: /* (non-Javadoc)
335: * @see java.util.Map#remove(java.lang.Object)
336: */
337: public Object remove(Object key) {
338: throw new Error("Not implemented");
339: }
340:
341: /* (non-Javadoc)
342: * @see java.util.Map#put(java.lang.Object, java.lang.Object)
343: */
344: public Object put(Object key, Object value) {
345: throw new Error("Not implemented");
346: }
347:
348: }
|