001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2002 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program 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
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki.auth.authorize;
021:
022: import java.io.BufferedWriter;
023: import java.io.File;
024: import java.io.FileNotFoundException;
025: import java.io.FileOutputStream;
026: import java.io.IOException;
027: import java.io.OutputStreamWriter;
028: import java.security.Principal;
029: import java.text.DateFormat;
030: import java.text.ParseException;
031: import java.text.SimpleDateFormat;
032: import java.util.Collection;
033: import java.util.Date;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.Map;
037: import java.util.Properties;
038:
039: import javax.xml.parsers.DocumentBuilderFactory;
040: import javax.xml.parsers.ParserConfigurationException;
041:
042: import org.apache.commons.lang.StringEscapeUtils;
043: import org.apache.log4j.Logger;
044: import org.w3c.dom.Document;
045: import org.w3c.dom.Element;
046: import org.w3c.dom.NodeList;
047: import org.xml.sax.SAXException;
048:
049: import com.ecyrd.jspwiki.NoRequiredPropertyException;
050: import com.ecyrd.jspwiki.WikiEngine;
051: import com.ecyrd.jspwiki.auth.NoSuchPrincipalException;
052: import com.ecyrd.jspwiki.auth.WikiPrincipal;
053: import com.ecyrd.jspwiki.auth.WikiSecurityException;
054:
055: /**
056: * <p>
057: * GroupDatabase implementation for loading, persisting and storing wiki groups,
058: * using an XML file for persistence. Group entries are simple
059: * <code><group></code> elements under the root. Each group member is
060: * representated by a <code><member></code> element. For example:
061: * </p>
062: * <blockquote><code>
063: * <groups><br/>
064: * <group name="TV" created="Jun 20, 2006 2:50:54 PM" lastModified="Jan 21, 2006 2:50:54 PM"><br/>
065: * <member principal="Archie Bunker" /><br/>
066: * <member principal="BullwinkleMoose" /><br/>
067: * <member principal="Fred Friendly" /><br/>
068: * </group><br/>
069: * <group name="Literature" created="Jun 22, 2006 2:50:54 PM" lastModified="Jan 23, 2006 2:50:54 PM"><br/>
070: * <member principal="Charles Dickens" /><br/>
071: * <member principal="Homer" /><br/>
072: * </group><br/>
073: * </groups>
074: * </code></blockquote>
075: * @author Andrew Jaquith
076: * @since 2.4.17
077: */
078: public class XMLGroupDatabase implements GroupDatabase {
079: protected static final Logger log = Logger
080: .getLogger(XMLGroupDatabase.class);
081:
082: /**
083: * The jspwiki.properties property specifying the file system location of
084: * the group database.
085: */
086: public static final String PROP_DATABASE = "jspwiki.xmlGroupDatabaseFile";
087:
088: private static final String DEFAULT_DATABASE = "groupdatabase.xml";
089:
090: private static final String CREATED = "created";
091:
092: private static final String CREATOR = "creator";
093:
094: private static final String GROUP_TAG = "group";
095:
096: private static final String GROUP_NAME = "name";
097:
098: private static final String LAST_MODIFIED = "lastModified";
099:
100: private static final String MODIFIER = "modifier";
101:
102: private static final String MEMBER_TAG = "member";
103:
104: private static final String PRINCIPAL = "principal";
105:
106: private Document m_dom = null;
107:
108: private DateFormat m_defaultFormat = DateFormat
109: .getDateTimeInstance();
110:
111: private DateFormat m_format = new SimpleDateFormat(
112: "yyyy.MM.dd 'at' HH:mm:ss:SSS z");
113:
114: private File m_file = null;
115:
116: private WikiEngine m_engine = null;
117:
118: private Map m_groups = new HashMap();
119:
120: /**
121: * No-op method that in previous versions of JSPWiki was intended to
122: * atomically commit changes to the user database. Now, the
123: * {@link #save(Group, Principal)} and {@link #delete(Group)} methods
124: * are atomic themselves.
125: * @throws WikiSecurityException never...
126: * @deprecated there is no need to call this method because the save and
127: * delete methods contain their own commit logic
128: */
129: public void commit() throws WikiSecurityException {
130: }
131:
132: /**
133: * Looks up and deletes a {@link Group} from the group database. If the
134: * group database does not contain the supplied Group. this method throws a
135: * {@link NoSuchPrincipalException}. The method commits the results
136: * of the delete to persistent storage.
137: * @param group the group to remove
138: * @throws WikiSecurityException if the database does not contain the
139: * supplied group (thrown as {@link NoSuchPrincipalException}) or if
140: * the commit did not succeed
141: */
142: public void delete(Group group) throws WikiSecurityException {
143: String index = group.getName();
144: boolean exists = m_groups.containsKey(index);
145:
146: if (!exists) {
147: throw new NoSuchPrincipalException("Not in database: "
148: + group.getName());
149: }
150:
151: m_groups.remove(index);
152:
153: // Commit to disk
154: saveDOM();
155: }
156:
157: /**
158: * Returns all wiki groups that are stored in the GroupDatabase as an array
159: * of Group objects. If the database does not contain any groups, this
160: * method will return a zero-length array. This method causes back-end
161: * storage to load the entire set of group; thus, it should be called
162: * infrequently (e.g., at initialization time).
163: * @return the wiki groups
164: * @throws WikiSecurityException if the groups cannot be returned by the back-end
165: */
166: public Group[] groups() throws WikiSecurityException {
167: buildDOM();
168: Collection groups = m_groups.values();
169: return (Group[]) groups.toArray(new Group[groups.size()]);
170: }
171:
172: /**
173: * Initializes the group database based on values from a Properties object.
174: * The properties object must contain a file path to the XML database file
175: * whose key is {@link #PROP_DATABASE}.
176: * @param engine the wiki engine
177: * @param props the properties used to initialize the group database
178: * @throws NoRequiredPropertyException if the user database cannot be
179: * located, parsed, or opened
180: * @throws WikiSecurityException if the database could not be initialized successfully
181: */
182: public void initialize(WikiEngine engine, Properties props)
183: throws NoRequiredPropertyException, WikiSecurityException {
184: m_engine = engine;
185:
186: File defaultFile = null;
187: if (engine.getRootPath() == null) {
188: log.warn("Cannot identify JSPWiki root path");
189: defaultFile = new File("WEB-INF/" + DEFAULT_DATABASE)
190: .getAbsoluteFile();
191: } else {
192: defaultFile = new File(engine.getRootPath() + "/WEB-INF/"
193: + DEFAULT_DATABASE);
194: }
195:
196: // Get database file location
197: String file = props.getProperty(PROP_DATABASE);
198: if (file == null) {
199: log.error("XML group database property " + PROP_DATABASE
200: + " not found; trying " + defaultFile);
201: m_file = defaultFile;
202: } else {
203: m_file = new File(file);
204: }
205:
206: log.info("XML group database at " + m_file.getAbsolutePath());
207:
208: // Read DOM
209: buildDOM();
210: }
211:
212: /**
213: * Saves a Group to the group database. Note that this method <em>must</em>
214: * fail, and throw an <code>IllegalArgumentException</code>, if the
215: * proposed group is the same name as one of the built-in Roles: e.g.,
216: * Admin, Authenticated, etc. The database is responsible for setting
217: * create/modify timestamps, upon a successful save, to the Group.
218: * The method commits the results of the delete to persistent storage.
219: * @param group the Group to save
220: * @param modifier the user who saved the Group
221: * @throws WikiSecurityException if the Group could not be saved successfully
222: */
223: public void save(Group group, Principal modifier)
224: throws WikiSecurityException {
225: if (group == null || modifier == null) {
226: throw new IllegalArgumentException(
227: "Group or modifier cannot be null.");
228: }
229:
230: checkForRefresh();
231:
232: String index = group.getName();
233: boolean isNew = !(m_groups.containsKey(index));
234: Date modDate = new Date(System.currentTimeMillis());
235: if (isNew) {
236: // If new, set created info
237: group.setCreated(modDate);
238: group.setCreator(modifier.getName());
239: }
240: group.setModifier(modifier.getName());
241: group.setLastModified(modDate);
242:
243: // Add the group to the 'saved' list
244: m_groups.put(index, group);
245:
246: // Commit to disk
247: saveDOM();
248: }
249:
250: private void buildDOM() throws WikiSecurityException {
251: DocumentBuilderFactory factory = DocumentBuilderFactory
252: .newInstance();
253: factory.setValidating(false);
254: factory.setExpandEntityReferences(false);
255: factory.setIgnoringComments(true);
256: factory.setNamespaceAware(false);
257: try {
258: m_dom = factory.newDocumentBuilder().parse(m_file);
259: log.debug("Database successfully initialized");
260: m_lastModified = m_file.lastModified();
261: m_lastCheck = System.currentTimeMillis();
262: } catch (ParserConfigurationException e) {
263: log.error("Configuration error: " + e.getMessage());
264: } catch (SAXException e) {
265: log.error("SAX error: " + e.getMessage());
266: } catch (FileNotFoundException e) {
267: log
268: .info("Group database not found; creating from scratch...");
269: } catch (IOException e) {
270: log.error("IO error: " + e.getMessage());
271: }
272: if (m_dom == null) {
273: try {
274: //
275: // Create the DOM from scratch
276: //
277: m_dom = factory.newDocumentBuilder().newDocument();
278: m_dom.appendChild(m_dom.createElement("groups"));
279: } catch (ParserConfigurationException e) {
280: log.fatal("Could not create in-memory DOM");
281: }
282: }
283:
284: // Ok, now go and read this sucker in
285: if (m_dom != null) {
286: NodeList groupNodes = m_dom.getElementsByTagName(GROUP_TAG);
287: for (int i = 0; i < groupNodes.getLength(); i++) {
288: Element groupNode = (Element) groupNodes.item(i);
289: String groupName = groupNode.getAttribute(GROUP_NAME);
290: if (groupName == null) {
291: log
292: .warn("Detected null group name in XMLGroupDataBase. Check your group database.");
293: } else {
294: Group group = buildGroup(groupNode, groupName);
295: m_groups.put(groupName, group);
296: }
297: }
298: }
299: }
300:
301: private long m_lastCheck = 0;
302: private long m_lastModified = 0;
303:
304: private void checkForRefresh() {
305: long time = System.currentTimeMillis();
306:
307: if (time - m_lastCheck > 60 * 1000L) {
308: long lastModified = m_file.lastModified();
309:
310: if (lastModified > m_lastModified) {
311: try {
312: buildDOM();
313: } catch (WikiSecurityException e) {
314: log.error("Could not refresh DOM", e);
315: }
316: }
317: }
318: }
319:
320: /**
321: * Constructs a Group based on a DOM group node.
322: * @param groupNode the node in the DOM containing the node
323: * @param name the name of the group
324: * @throws NoSuchPrincipalException
325: * @throws WikiSecurityException
326: */
327: private Group buildGroup(Element groupNode, String name) {
328: // It's an error if either param is null (very odd)
329: if (groupNode == null || name == null) {
330: throw new IllegalArgumentException(
331: "DOM element or name cannot be null.");
332: }
333:
334: // Construct a new group
335: Group group = new Group(name, m_engine.getApplicationName());
336:
337: // Get the users for this group, and add them
338: NodeList members = groupNode.getElementsByTagName(MEMBER_TAG);
339: for (int i = 0; i < members.getLength(); i++) {
340: Element memberNode = (Element) members.item(i);
341: String principalName = memberNode.getAttribute(PRINCIPAL);
342: Principal member = new WikiPrincipal(principalName);
343: group.add(member);
344: }
345:
346: // Add the created/last-modified info
347: String creator = groupNode.getAttribute(CREATOR);
348: String created = groupNode.getAttribute(CREATED);
349: String modifier = groupNode.getAttribute(MODIFIER);
350: String modified = groupNode.getAttribute(LAST_MODIFIED);
351: try {
352: group.setCreated(m_format.parse(created));
353: group.setLastModified(m_format.parse(modified));
354: } catch (ParseException e) {
355: // If parsing failed, use the platform default
356: try {
357: group.setCreated(m_defaultFormat.parse(created));
358: group.setLastModified(m_defaultFormat.parse(modified));
359: } catch (ParseException e2) {
360: log.warn("Could not parse 'created' or 'lastModified' "
361: + "attribute for " + " group'"
362: + group.getName() + "'."
363: + " It may have been tampered with.");
364: }
365: }
366: group.setCreator(creator);
367: group.setModifier(modifier);
368: return group;
369: }
370:
371: private void saveDOM() throws WikiSecurityException {
372: if (m_dom == null) {
373: log.fatal("Group database doesn't exist in memory.");
374: }
375:
376: File newFile = new File(m_file.getAbsolutePath() + ".new");
377: try {
378: BufferedWriter io = new BufferedWriter(
379: new OutputStreamWriter(
380: new FileOutputStream(newFile), "UTF-8"));
381:
382: // Write the file header and document root
383: io.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
384: io.write("<groups>\n");
385:
386: // Write each profile as a <group> node
387: Collection groups = m_groups.values();
388: for (Iterator it = groups.iterator(); it.hasNext();) {
389: Group group = (Group) it.next();
390: io.write(" <" + GROUP_TAG + " ");
391: io.write(GROUP_NAME);
392: io.write("=\""
393: + StringEscapeUtils.escapeXml(group.getName())
394: + "\" ");
395: io.write(CREATOR);
396: io.write("=\""
397: + StringEscapeUtils.escapeXml(group
398: .getCreator()) + "\" ");
399: io.write(CREATED);
400: io.write("=\"" + m_format.format(group.getCreated())
401: + "\" ");
402: io.write(MODIFIER);
403: io.write("=\"" + group.getModifier() + "\" ");
404: io.write(LAST_MODIFIED);
405: io.write("=\""
406: + m_format.format(group.getLastModified())
407: + "\"");
408: io.write(">\n");
409:
410: // Write each member as a <member> node
411: Principal[] members = group.members();
412: for (int j = 0; j < members.length; j++) {
413: Principal member = members[j];
414: io.write(" <" + MEMBER_TAG + " ");
415: io.write(PRINCIPAL);
416: io.write("=\""
417: + StringEscapeUtils.escapeXml(member
418: .getName()) + "\" ");
419: io.write("/>\n");
420: }
421:
422: // Close tag
423: io.write(" </" + GROUP_TAG + ">\n");
424: }
425: io.write("</groups>");
426: io.close();
427: } catch (IOException e) {
428: throw new WikiSecurityException(e.getLocalizedMessage());
429: }
430:
431: // Copy new file over old version
432: File backup = new File(m_file.getAbsolutePath() + ".old");
433: if (backup.exists()) {
434: if (!backup.delete()) {
435: log
436: .error("Could not delete old group database backup: "
437: + backup);
438: }
439: }
440: if (!m_file.renameTo(backup)) {
441: log.error("Could not create group database backup: "
442: + backup);
443: }
444: if (!newFile.renameTo(m_file)) {
445: log.error("Could not save database: " + backup
446: + " restoring backup.");
447: if (!backup.renameTo(m_file)) {
448: log
449: .error("Restore failed. Check the file permissions.");
450: }
451: log.error("Could not save database: " + m_file
452: + ". Check the file permissions");
453: }
454: }
455:
456: }
|