001: /*
002: * JOSSO: Java Open Single Sign-On
003: *
004: * Copyright 2004-2008, Atricore, Inc.
005: *
006: * This is free software; you can redistribute it and/or modify it
007: * under the terms of the GNU Lesser General Public License as
008: * published by the Free Software Foundation; either version 2.1 of
009: * the License, or (at your option) any later version.
010: *
011: * This software is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this software; if not, write to the Free
018: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
019: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
020: */
021: package org.josso.util.config;
022:
023: import org.apache.commons.logging.Log;
024: import org.apache.commons.logging.LogFactory;
025: import org.w3c.dom.Node;
026: import org.xml.sax.SAXException;
027: import org.xmldb.common.xml.queries.XUpdateQuery;
028: import org.xmldb.xupdate.lexus.XUpdateQueryImpl;
029:
030: import javax.xml.parsers.DocumentBuilder;
031: import javax.xml.parsers.DocumentBuilderFactory;
032: import javax.xml.parsers.ParserConfigurationException;
033: import javax.xml.transform.*;
034: import javax.xml.transform.dom.DOMSource;
035: import javax.xml.transform.stream.StreamResult;
036: import java.io.*;
037: import java.util.Calendar;
038: import java.util.Date;
039: import java.text.DateFormat;
040: import java.text.SimpleDateFormat;
041:
042: /**
043: * This is a base configuration handler that uses XUpdate to add, update and remove elements from a XML files.
044: * The XML file to be updated is referred by the ConfigurationContext instance related to this handler.
045: *
046: * @author <a href="mailto:sgonzalez@josso.org">Sebastian Gonzalez Oyuela</a>
047: * @version $Id: XUpdateConfigurationHandler.java 508 2008-02-18 13:32:29Z sgonzalez $
048: */
049: public class XUpdateConfigurationHandler implements
050: ConfigurationHandler {
051:
052: // log4j logger instance.
053: private static final Log logger = LogFactory
054: .getLog(XUpdateConfigurationHandler.class);
055:
056: private static final DateFormat df = new SimpleDateFormat(
057: "yyyy-MM-dd_HH_mm_ss_SSS");
058:
059: // The configuration context related to the handler.
060: private ConfigurationContext _ctx;
061:
062: /**
063: * XUPDATE constant used to begin all queries.
064: */
065: public static final String XUPDATE_START = "<xupdate:modifications version=\"1.0\"\n"
066: + " xmlns:xupdate=\"http://www.xmldb.org/xupdate\">\n";
067:
068: /**
069: * XUPDATE constant used to end all queries.
070: */
071: public static final String XUPDATE_END = "\n</xupdate:modifications> ";
072:
073: /**
074: * XPath expression to find where existing elements are found in the document.
075: */
076: private String elementsBaseLocation;
077:
078: /**
079: * XPath expression to find where new elements will be inserted AFTER in the document (as siblings)
080: */
081: private String newElementsBaseLocation;
082:
083: /**
084: * @param ctx The configuration context used by this handler.
085: * @param elementsBaseLocation XPath expression to find where existing elements are found in the document.
086: * @param newElementsBaseLocation XPath expression to find where new elements will be inserted AFTER in the document (as siblings)
087: */
088: public XUpdateConfigurationHandler(ConfigurationContext ctx,
089: String elementsBaseLocation, String newElementsBaseLocation) {
090: this (elementsBaseLocation, newElementsBaseLocation);
091: _ctx = ctx;
092: }
093:
094: /**
095: * @param elementsBaseLocation XPath expression to find where existing elements are found in the document.
096: * @param newElementsBaseLocation XPath expression to find where new elements will be inserted AFTER in the document (as siblings)
097: */
098: public XUpdateConfigurationHandler(String elementsBaseLocation,
099: String newElementsBaseLocation) {
100: // Setup XUpdate factory.
101: System
102: .setProperty(
103: "org.xmldb.common.xml.queries.XPathQueryFactory",
104: "org.xmldb.common.xml.queries.xalan2.XPathQueryFactoryImpl");
105:
106: this .elementsBaseLocation = elementsBaseLocation;
107: this .newElementsBaseLocation = newElementsBaseLocation;
108: }
109:
110: /**
111: * Setter for this handler configuration context.
112: */
113: public void setSSOConfigurationContext(ConfigurationContext ctx) {
114: _ctx = ctx;
115: }
116:
117: /**
118: * Getter for this handler configuration context.
119: */
120: public ConfigurationContext getSSOConfigurationContext() {
121: return _ctx;
122: }
123:
124: /**
125: * This method will add/update the specified element based on it's old and new values.
126: * If the oldValue is null, this method will insert the element, if it's not, it will try to update an existing element.
127: *
128: * @param element
129: * @param oldValue
130: * @param newValue
131: */
132: public void saveElement(String element, String oldValue,
133: String newValue) {
134:
135: if (!_ctx.isConfigurationUpdatable())
136: return;
137:
138: // Log what we're doing ....
139: if (logger.isDebugEnabled()) {
140: logger.debug("saveElement : " + element + " [" + oldValue
141: + "/" + newValue + "] at "
142: + this .elementsBaseLocation + " "
143: + this .newElementsBaseLocation);
144: }
145:
146: try {
147: // Update an element in a josso configuration file :
148: String qry;
149: if (oldValue == null) {
150: // The attribute is null, add a new element.
151: qry = buildXInsertAfterElementQueryString(
152: this .newElementsBaseLocation, element,
153: unicodeEscape(newValue));
154: } else {
155: // The attribute has a value, update the existing element.
156: qry = buildXUPdateElementQueryString(
157: this .elementsBaseLocation + "/" + element,
158: unicodeEscape(newValue));
159: }
160: this .updateConfiguration(qry);
161: } catch (Exception e) {
162: logger.error("Can't update configuration element for : "
163: + element + ", new value : " + newValue + " :\n"
164: + e.getMessage(), e);
165: }
166: }
167:
168: /**
169: * This method will remove the specified element. If the configuration is not updatable, this method does nothing.
170: *
171: * @see ConfigurationContext#isConfigurationUpdatable()
172: */
173: public void removeElement(String element) {
174: try {
175:
176: if (!_ctx.isConfigurationUpdatable())
177: return;
178:
179: // Log what we're doing ....
180: if (logger.isDebugEnabled()) {
181: logger.debug("removeElement : " + element + " at "
182: + this .elementsBaseLocation);
183: }
184:
185: // Delete an element from the configuration file :
186: String qry = buildXDeleteElementQuery(
187: this .elementsBaseLocation, element);
188: this .updateConfiguration(qry);
189:
190: } catch (Exception e) {
191: logger.error("Can't update configuration element for : "
192: + element + " :\n" + e.getMessage(), e);
193: }
194: }
195:
196: /**
197: * Updates the element located after the xpathExpr. If the configuration is not updatable, the method does nothing.
198: * It checks that configuration backup is enable.
199: *
200: * @param qry the XUpdate query to be used to update the configuration file.
201: *
202: * @see ConfigurationContext#isConfigurationUpdatable()
203: * @see ConfigurationContext#isBackupEnabled()
204: */
205: protected void updateConfiguration(String qry) throws Exception {
206:
207: if (!_ctx.isConfigurationUpdatable())
208: return;
209:
210: // Read the configuration file.
211: Node jossoCfgDocument = readConfigFile();
212:
213: // Assume any exception is due to a "no nodes selected" problem ...!?
214: XUpdateQuery xq = new XUpdateQueryImpl();
215: xq.setQString(qry);
216:
217: xq.execute(jossoCfgDocument);
218:
219: writeConfigFile(jossoCfgDocument);
220:
221: }
222:
223: /**
224: * This method reads a configuration into a DOM tree. The file is retrieved from the ConfigurationContext instance.
225: *
226: * @return the DOM tree representing the configuration file.
227: */
228: protected Node readConfigFile()
229: throws ParserConfigurationException, IOException,
230: SAXException {
231:
232: // parse the document file
233: Node myDocument;
234: DocumentBuilderFactory parserFactory = DocumentBuilderFactory
235: .newInstance();
236:
237: parserFactory.setValidating(false);
238: parserFactory.setNamespaceAware(true);
239: parserFactory.setIgnoringElementContentWhitespace(true);
240:
241: File file = _ctx.getConfigurationFile();
242:
243: if (!file.exists())
244: throw new IOException("File not found"
245: + file.getAbsolutePath());
246:
247: DocumentBuilder builder = parserFactory.newDocumentBuilder();
248: myDocument = builder.parse(file);
249:
250: return myDocument;
251:
252: }
253:
254: /**
255: * Makes a backup of the configuration file to disk.
256: */
257: protected void backupConfigFile() throws Exception {
258:
259: String timestamp = df.format(new Date());
260:
261: FileInputStream fis = null;
262: FileOutputStream fos = null;
263: try {
264: fis = new FileInputStream(_ctx.getConfigurationFile());
265: fos = new FileOutputStream(_ctx.getConfigurationFile()
266: .getAbsolutePath()
267: + "." + timestamp + ".bkp");
268: // Copy the file ...
269: byte[] buf = new byte[1024];
270: int i;
271: while ((i = fis.read(buf)) != -1) {
272: fos.write(buf, 0, i);
273: }
274:
275: } catch (Exception e) {
276: logger.error("Can't backup configuration file : "
277: + _ctx.getConfigurationFile().getName() + " : "
278: + e.getMessage() != null ? e.getMessage() : e
279: .toString());
280: throw e;
281:
282: } finally {
283:
284: try {
285: if (fis != null)
286: fis.close();
287: } catch (Exception e) { /**/
288: }
289: try {
290: if (fos != null)
291: fos.close();
292: } catch (Exception e) { /**/
293: }
294: }
295:
296: }
297:
298: /**
299: * Save this file to disk
300: */
301: protected void writeConfigFile(Node document) throws Exception {
302: writeConfigFile(document, _ctx.getConfigurationFile());
303: }
304:
305: /**
306: * Save this file to disk
307: */
308: protected void writeConfigFile(Node document, File file)
309: throws Exception {
310:
311: // Backup configuration file.
312: if (_ctx.isBackupEnabled())
313: backupConfigFile();
314:
315: // Write the DOM document to the file
316: java.io.FileOutputStream fos = null;
317: try {
318: fos = new FileOutputStream(file, false);
319: Result result = new StreamResult(fos);
320: Source source = new DOMSource(document);
321: Transformer xformer = TransformerFactory.newInstance()
322: .newTransformer();
323: xformer.transform(source, result);
324: } catch (java.io.IOException e) {
325: logger.error(e, e);
326: } finally {
327: if (fos != null)
328: try {
329: fos.close();
330: } catch (java.io.IOException e) { /**/
331: }
332: }
333:
334: }
335:
336: // Utils to build different type of queries :
337:
338: /**
339: * Builds an XUpdate query string to update an element's value.
340: *
341: * @param xpathExpr the XPath expression to locate the element.
342: * @param newValue the new element value, must be already escaped.
343: */
344: protected String buildXUPdateElementQueryString(String xpathExpr,
345: String newValue) {
346: // Build a new XUPDATE expression :
347: String qry = XUPDATE_START + "\t<xupdate:update select=\""
348: + xpathExpr + "\">" + newValue + "</xupdate:update>"
349: + XUPDATE_END;
350:
351: if (logger.isDebugEnabled())
352: logger.debug("buildXUPdateElementQueryString(" + xpathExpr
353: + "," + newValue + ") = \n" + qry);
354:
355: return qry;
356: }
357:
358: /**
359: * Builds a XUpdate query string to append a new element to the document.
360: *
361: * @param xpathExpr the XPath expression to locate the element where the new element will be appended.
362: * @param element the new element name
363: * @param value new element value, must be already escaped.
364: */
365: protected String buildXAppendElementQueryString(String xpathExpr,
366: String element, String value) {
367: String qry = XUPDATE_START + "\t<xupdate:append select=\""
368: + xpathExpr + "\" >\n" + "\t\t<xupdate:element name=\""
369: + element + "\">" + value + "</xupdate:element>\n"
370: + "\t</xupdate:append>" + XUPDATE_END;
371:
372: if (logger.isDebugEnabled())
373: logger.debug("buildXAppendElementQueryString(" + xpathExpr
374: + "," + element + "," + value + ") = \n" + qry);
375:
376: return qry;
377:
378: }
379:
380: /**
381: * Builds a XUpdate query string to insert an element after another.
382: *
383: * @param xpathExpr the XPath expression to locate the element where the new element will be inserted.
384: * @param element the new element name
385: * @param value new element value, must be already escaped.
386: */
387: protected String buildXInsertAfterElementQueryString(
388: String xpathExpr, String element, String value) {
389: String qry = XUPDATE_START
390: + "\t<xupdate:insert-after select=\"" + xpathExpr
391: + "\" >\n" + "\t\t<xupdate:element name=\"" + element
392: + "\">" + value + "</xupdate:element>\n"
393: + "\t</xupdate:insert-after>" + XUPDATE_END;
394:
395: if (logger.isDebugEnabled())
396: logger.debug("buildXAppendElementQueryString(" + xpathExpr
397: + "," + element + "," + value + ") = \n" + qry);
398:
399: return qry;
400:
401: }
402:
403: /**
404: * Builds a XUpdate query string to append a new element with the specified XML as body.
405: * @param xpathExpr
406: * @param element
407: * @param xml
408: */
409: protected String buildXAppendElementXMLQueryString(
410: String xpathExpr, String element, String xml) {
411: String qry = XUPDATE_START + "\t<xupdate:append select=\""
412: + xpathExpr + "\" >\n" + "\t\t<xupdate:element name=\""
413: + element + "\">\n" + xml + "\n"
414: + "\t\t</xupdate:element>\n" + "\t</xupdate:append>"
415: + XUPDATE_END;
416:
417: if (logger.isDebugEnabled())
418: logger.debug("buildXInsertXMLQueryString(" + xpathExpr
419: + "," + element + "," + xml + ") = \n" + qry);
420:
421: return qry;
422: }
423:
424: /**
425: * Builds a XUpdate query string to delete the specified element.
426: *
427: * @param xpathExpr
428: * @param element
429: */
430: protected String buildXDeleteElementQuery(String xpathExpr,
431: String element) {
432: String qry = XUPDATE_START + "\t<xupdate:remove select=\""
433: + xpathExpr + "/" + element + "\"/>" + XUPDATE_END;
434:
435: if (logger.isDebugEnabled())
436: logger.debug("buildXDeleteElementQuery(" + xpathExpr + ","
437: + element + ") = \n" + qry);
438:
439: return qry;
440: }
441:
442: protected String getElementsBaseLocation() {
443: return elementsBaseLocation;
444: }
445:
446: protected String getNewElementsBaseLocation() {
447: return newElementsBaseLocation;
448: }
449:
450: /**
451: * This will scape all spetial chars like <, >, &, \, " and unicode chars.
452: */
453: public String unicodeEscape(String v) {
454:
455: StringWriter w = new StringWriter();
456:
457: int len = v.length();
458: for (int j = 0; j < len; j++) {
459: char c = v.charAt(j);
460: switch (c) {
461: case '&':
462: w.write("&");
463: break;
464: case '<':
465: w.write("<");
466: break;
467: case '>':
468: w.write(">");
469: break;
470: case '\'':
471: w.write("'");
472: break;
473: case '"':
474: w.write(""");
475: break;
476: default:
477: if (canEncode(c)) {
478: w.write(c);
479: } else {
480: w.write("&#");
481: w.write(Integer.toString(c));
482: w.write(';');
483: }
484: break;
485: }
486: }
487: ByteArrayInputStream d;
488: return w.toString();
489: }
490:
491: public static boolean canEncode(char c) {
492: return c < 127;
493: }
494:
495: }
|