| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| /* $Id: RCML.java 473861 2006-11-12 03:51:14Z gregor $ */ |
| |
| package org.apache.lenya.cms.repository; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Vector; |
| |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.excalibur.source.SourceResolver; |
| import org.apache.lenya.cms.cocoon.source.SourceUtil; |
| import org.apache.lenya.cms.rc.CheckInEntry; |
| import org.apache.lenya.cms.rc.CheckOutEntry; |
| import org.apache.lenya.cms.rc.RCML; |
| import org.apache.lenya.cms.rc.RCMLEntry; |
| import org.apache.lenya.cms.rc.RevisionControlException; |
| import org.apache.lenya.util.Assert; |
| import org.apache.lenya.xml.DocumentHelper; |
| import org.apache.lenya.xml.NamespaceHelper; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| |
| /** |
| * Handle with the RCML file |
| */ |
| public class SourceNodeRCML implements RCML { |
| |
| protected static final String NAMESPACE = ""; |
| |
| private boolean dirty = false; |
| private int maximalNumberOfEntries = 5; |
| private Vector entries; |
| |
| private ServiceManager manager; |
| |
| private String contentSourceUri; |
| private String metaSourceUri; |
| |
| private static Map ELEMENTS = new HashMap(); |
| protected static final String ELEMENT_CHECKIN = "CheckIn"; |
| protected static final String ELEMENT_CHECKOUT = "CheckOut"; |
| protected static final String ELEMENT_BACKUP = "Backup"; |
| protected static final String ELEMENT_TIME = "Time"; |
| protected static final String ELEMENT_VERSION = "Version"; |
| protected static final String ELEMENT_IDENTITY = "Identity"; |
| protected static final String ELEMENT_XPSREVISIONCONTROL = "XPSRevisionControl"; |
| protected static final String ELEMENT_SESSION = "session"; |
| |
| protected static final String ATTR_BACKUP = "backup"; |
| protected static final String ATTR_TIME = "time"; |
| protected static final String ATTR_VERSION = "version"; |
| protected static final String ATTR_IDENTITY = "identity"; |
| protected static final String ATTR_SESSION = "session"; |
| |
| { |
| ELEMENTS.put(new Short(ci), ELEMENT_CHECKIN); |
| ELEMENTS.put(new Short(co), ELEMENT_CHECKOUT); |
| } |
| |
| /** |
| * @param contentSourceUri The content source URI. |
| * @param metaSourceUri The meta source URI. |
| * @param manager The service manager. |
| */ |
| public SourceNodeRCML(String contentSourceUri, String metaSourceUri, ServiceManager manager) { |
| this.maximalNumberOfEntries = 200; |
| this.maximalNumberOfEntries = (2 * this.maximalNumberOfEntries) + 1; |
| this.manager = manager; |
| this.contentSourceUri = contentSourceUri; |
| this.metaSourceUri = metaSourceUri; |
| } |
| |
| protected static final String RCML_EXTENSION = ".rcml"; |
| |
| private static final String ALL_SESSIONS = "unrestricted"; |
| |
| protected String getRcmlSourceUri() { |
| return this.contentSourceUri + RCML_EXTENSION; |
| } |
| |
| /** |
| * Call the method write, if the document is dirty |
| * |
| * @throws IOException if an error occurs |
| * @throws Exception if an error occurs |
| */ |
| protected void finalize() throws IOException, Exception { |
| if (this.isDirty()) { |
| write(); |
| } |
| } |
| |
| /** |
| * Write the XML RCML-document in the RCML-file. |
| * @throws RevisionControlException if an error occurs |
| */ |
| public synchronized void write() throws RevisionControlException { |
| NamespaceHelper helper = saveToXml(); |
| Assert.notNull("XML document", helper); |
| try { |
| SourceUtil.writeDOM(helper.getDocument(), getRcmlSourceUri(), this.manager); |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| clearDirty(); |
| } |
| |
| /** |
| * Write a new entry for a check out or a check in the RCML-File made by the user with identity |
| * at time |
| * @param node The node. |
| * @param type co for a check out, ci for a check in |
| * @param time |
| * @param backup Create backup element (only considered for check-in entries). |
| * @param newVersion If the revision number shall be increased (only considered for check-in |
| * entries). |
| * @param restrictedToSession If the check-out is restricted to the session (only considered for |
| * check-out entries). |
| * @throws RevisionControlException if an error occurs |
| */ |
| public synchronized void checkOutIn(Node node, short type, long time, boolean backup, |
| boolean newVersion, boolean restrictedToSession) throws RevisionControlException { |
| |
| String identity = node.getSession().getIdentity().getUser().getId(); |
| |
| Vector entries = getEntries(); |
| if (entries.size() == 0) { |
| if (type == ci) { |
| throw new IllegalStateException("Can't check in - not checked out."); |
| } |
| } else { |
| RCMLEntry latestEntry = getLatestEntry(); |
| if (type == latestEntry.getType()) { |
| String elementName = (String) ELEMENTS.get(new Short(type)); |
| throw new IllegalStateException("RCML entry type <" + elementName |
| + "> not allowed twice in a row. Before: [" + latestEntry.getIdentity() |
| + "], now: [" + identity + "], node: [" + this.contentSourceUri + "]"); |
| } |
| } |
| |
| String sessionId; |
| if (type == RCML.co && !restrictedToSession) { |
| sessionId = ALL_SESSIONS; |
| } |
| else { |
| sessionId = node.getSession().getId(); |
| } |
| |
| RCMLEntry entry; |
| switch (type) { |
| case RCML.ci: |
| int version = 0; |
| CheckInEntry latestEntry = getLatestCheckInEntry(); |
| if (latestEntry != null) { |
| version = latestEntry.getVersion(); |
| } |
| if (newVersion) { |
| version++; |
| } |
| entry = new CheckInEntry(sessionId, identity, time, version, backup); |
| break; |
| case RCML.co: |
| entry = new CheckOutEntry(sessionId, identity, time); |
| break; |
| default: |
| throw new IllegalArgumentException("No such type: [" + type + "]"); |
| } |
| |
| entries.add(0, entry); |
| setDirty(); |
| } |
| |
| protected Element saveToXml(NamespaceHelper helper, RCMLEntry entry) |
| throws RevisionControlException { |
| String elementName = (String) ELEMENTS.get(new Short(entry.getType())); |
| Element entryElement = helper.createElement(elementName); |
| |
| entryElement.setAttribute(ATTR_IDENTITY, entry.getIdentity()); |
| entryElement.setAttribute(ATTR_SESSION, entry.getSessionId()); |
| entryElement.setAttribute(ATTR_TIME, Long.toString(entry.getTime())); |
| |
| if (entry.getType() == ci) { |
| CheckInEntry checkInEntry = (CheckInEntry) entry; |
| entryElement.setAttribute(ATTR_VERSION, Integer.toString(checkInEntry.getVersion())); |
| if (checkInEntry.hasBackup()) { |
| entryElement.setAttribute(ATTR_BACKUP, "true"); |
| } |
| } |
| |
| return entryElement; |
| } |
| |
| protected NamespaceHelper saveToXml() throws RevisionControlException { |
| try { |
| NamespaceHelper helper = new NamespaceHelper(NAMESPACE, "", ELEMENT_XPSREVISIONCONTROL); |
| Element root = helper.getDocument().getDocumentElement(); |
| Vector entries = getEntries(); |
| for (Iterator i = entries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| Element element = saveToXml(helper, entry); |
| root.appendChild(element); |
| } |
| return helper; |
| } catch (Exception e) { |
| throw new RevisionControlException("Could create revision control XML [" |
| + getRcmlSourceUri() + "]", e); |
| } |
| } |
| |
| protected Element getLatestElement(NamespaceHelper helper, String type) |
| throws RevisionControlException { |
| Element parent = helper.getDocument().getDocumentElement(); |
| return helper.getFirstChild(parent, type); |
| } |
| |
| /** |
| * get the latest check out |
| * @return CheckOutEntry The entry of the check out |
| * @throws RevisionControlException if an error occurs |
| */ |
| public CheckOutEntry getLatestCheckOutEntry() throws RevisionControlException { |
| return (CheckOutEntry) getLatestEntry(RCML.co); |
| } |
| |
| /** |
| * get the latest check in |
| * @return CheckInEntry The entry of the check in |
| * @throws RevisionControlException if an error occurs |
| */ |
| public CheckInEntry getLatestCheckInEntry() throws RevisionControlException { |
| return (CheckInEntry) getLatestEntry(RCML.ci); |
| } |
| |
| /** |
| * get the latest entry (a check out or check in) |
| * @param type The type. |
| * @return RCMLEntry The entry of the check out/in |
| * @throws RevisionControlException if an error occurs |
| */ |
| public RCMLEntry getLatestEntry(short type) throws RevisionControlException { |
| Vector entries = getEntries(); |
| for (Iterator i = entries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| if (entry.getType() == type) { |
| return entry; |
| } |
| } |
| return null; |
| } |
| |
| public RCMLEntry getLatestEntry() throws RevisionControlException { |
| Vector entries = getEntries(); |
| if (entries.isEmpty()) { |
| return null; |
| } else { |
| return (RCMLEntry) entries.firstElement(); |
| } |
| } |
| |
| protected RCMLEntry getEntry(NamespaceHelper helper, Element element) { |
| if (element.hasAttribute(ATTR_IDENTITY)) { |
| String type = element.getLocalName(); |
| String sessionId = element.getAttribute(ATTR_SESSION); |
| String identity = element.getAttribute(ATTR_IDENTITY); |
| String timeString = element.getAttribute(ATTR_TIME); |
| long time = new Long(timeString).longValue(); |
| if (type.equals(ELEMENT_CHECKIN)) { |
| String versionString = element.getAttribute(ATTR_VERSION); |
| int version = new Integer(versionString).intValue(); |
| boolean backup = element.hasAttribute(ATTR_BACKUP); |
| return new CheckInEntry(sessionId, identity, time, version, backup); |
| } else if (type.equals(ELEMENT_CHECKOUT)) { |
| return new CheckOutEntry(sessionId, identity, time); |
| } else { |
| throw new RuntimeException("Unsupported RCML entry type: [" + type + "]"); |
| } |
| } else { |
| return getLegacyEntry(helper, element); |
| } |
| } |
| |
| protected RCMLEntry getLegacyEntry(NamespaceHelper helper, Element element) { |
| String type = element.getLocalName(); |
| String sessionId = getChildValue(helper, element, ELEMENT_SESSION, ""); |
| String identity = getChildValue(helper, element, ELEMENT_IDENTITY); |
| String timeString = getChildValue(helper, element, ELEMENT_TIME); |
| long time = new Long(timeString).longValue(); |
| if (type.equals(ELEMENT_CHECKIN)) { |
| String versionString = getChildValue(helper, element, ELEMENT_VERSION); |
| int version = new Integer(versionString).intValue(); |
| boolean backup = helper.getChildren(element, ELEMENT_BACKUP).length > 0; |
| return new CheckInEntry(sessionId, identity, time, version, backup); |
| } else if (type.equals(ELEMENT_CHECKOUT)) { |
| return new CheckOutEntry(sessionId, identity, time); |
| } else { |
| throw new RuntimeException("Unsupported RCML entry type: [" + type + "]"); |
| } |
| } |
| |
| protected String getChildValue(NamespaceHelper helper, Element element, String childName, |
| String defaultValue) { |
| Element child = DocumentHelper.getFirstChild(element, NAMESPACE, childName); |
| if (child == null) { |
| return defaultValue; |
| } else { |
| return DocumentHelper.getSimpleElementText(child); |
| } |
| } |
| |
| protected String getChildValue(NamespaceHelper helper, Element element, String childName) { |
| Element child = helper.getFirstChild(element, childName); |
| if (child == null) { |
| throw new RuntimeException("The element <" + element.getNodeName() |
| + "> has no child element <" + childName + ">. Source URI: [" |
| + getRcmlSourceUri() + "["); |
| } |
| return DocumentHelper.getSimpleElementText(child); |
| } |
| |
| /** |
| * get all check in and check out |
| * @return Vector of all check out and check in entries in this RCML-file |
| * @throws RevisionControlException if an error occurs |
| */ |
| public synchronized Vector getEntries() throws RevisionControlException { |
| if (this.entries == null) { |
| this.entries = new Vector(); |
| String uri = getRcmlSourceUri(); |
| try { |
| if (SourceUtil.exists(uri, this.manager)) { |
| Document xml = SourceUtil.readDOM(uri, this.manager); |
| NamespaceHelper helper = new NamespaceHelper(NAMESPACE, "", xml); |
| Element parent = xml.getDocumentElement(); |
| Element[] elements = helper.getChildren(parent); |
| for (int i = 0; i < elements.length; i++) { |
| RCMLEntry entry = getEntry(helper, elements[i]); |
| entries.add(entry); |
| } |
| } |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| } |
| return this.entries; |
| } |
| |
| /** |
| * get all backup entries |
| * @return Vector of all entries in this RCML-file with a backup |
| * @throws Exception if an error occurs |
| */ |
| public synchronized Vector getBackupEntries() throws Exception { |
| Vector entries = getEntries(); |
| Vector backupEntries = new Vector(); |
| for (Iterator i = entries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| if (entry.getType() == RCML.ci && ((CheckInEntry) entry).hasBackup()) { |
| backupEntries.add(entry); |
| } |
| } |
| return backupEntries; |
| } |
| |
| public synchronized void makeBackup(long time) throws RevisionControlException { |
| makeBackup(this.contentSourceUri, time); |
| makeBackup(this.metaSourceUri, time); |
| } |
| |
| protected synchronized void makeBackup(String sourceUri, long time) |
| throws RevisionControlException { |
| String backupSourceUri = getBackupSourceUri(sourceUri, time); |
| try { |
| if (SourceUtil.exists(sourceUri, manager)) { |
| SourceUtil.copy(this.manager, sourceUri, backupSourceUri); |
| } |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| } |
| |
| public synchronized void restoreBackup(Node node, long time) throws RevisionControlException { |
| SourceNode sourceNode = (SourceNode) node; |
| restoreBackup(sourceNode.getContentSource(), time); |
| restoreBackup(sourceNode.getMetaSource(), time); |
| } |
| |
| protected synchronized void restoreBackup(SourceWrapper wrapper, long time) |
| throws RevisionControlException { |
| String backupSourceUri = getBackupSourceUri(wrapper, time); |
| SourceResolver resolver = null; |
| try { |
| resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE); |
| SourceUtil.copy(resolver, backupSourceUri, wrapper.getOutputStream()); |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } finally { |
| if (resolver != null) { |
| this.manager.release(resolver); |
| } |
| } |
| } |
| |
| protected String getBackupSourceUri(SourceWrapper wrapper, long time) { |
| String uri = wrapper.getRealSourceUri(); |
| return getBackupSourceUri(uri, time); |
| } |
| |
| protected String getBackupSourceUri(String uri, long time) { |
| return uri + "." + time + ".bak"; |
| } |
| |
| /** |
| * Prune the list of entries and delete the corresponding backups. Limit the number of entries |
| * to the value maximalNumberOfEntries (2maxNumberOfRollbacks(configured)+1) |
| * @throws RevisionControlException if an error occurs |
| */ |
| public synchronized void pruneEntries() throws RevisionControlException { |
| Vector entries = getEntries(); |
| RCMLEntry[] array = (RCMLEntry[]) entries.toArray(new RCMLEntry[entries.size()]); |
| |
| for (int i = this.maximalNumberOfEntries; i < entries.size(); i++) { |
| // remove the backup file associated with this entry |
| RCMLEntry entry = array[i]; |
| if (entry.getType() == ci && ((CheckInEntry) entry).hasBackup()) { |
| long time = entry.getTime(); |
| deleteBackup(this.contentSourceUri, time); |
| deleteBackup(this.metaSourceUri, time); |
| } |
| this.entries.remove(entry); |
| } |
| setDirty(); |
| } |
| |
| protected synchronized void deleteBackup(String sourceUri, long time) |
| throws RevisionControlException { |
| String uri = getBackupSourceUri(sourceUri, time); |
| try { |
| SourceUtil.delete(uri, this.manager); |
| SourceUtil.deleteEmptyCollections(uri, this.manager); |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| } |
| |
| /** |
| * Check if the document is dirty |
| * @return boolean dirty |
| */ |
| public boolean isDirty() { |
| return this.dirty; |
| } |
| |
| /** |
| * Set the value dirty to true |
| */ |
| protected void setDirty() { |
| this.dirty = true; |
| } |
| |
| /** |
| * Set the value dirty to false |
| */ |
| protected void clearDirty() { |
| this.dirty = false; |
| } |
| |
| /** |
| * get the time's value of the backups |
| * @return String[] the times |
| * @throws Exception if an error occurs |
| */ |
| public String[] getBackupsTime() throws Exception { |
| |
| Vector entries = getEntries(); |
| List times = new ArrayList(); |
| for (Iterator i = entries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| if (entry.getType() == ci && ((CheckInEntry) entry).hasBackup()) { |
| times.add(Long.toString(entry.getTime())); |
| } |
| } |
| return (String[]) times.toArray(new String[times.size()]); |
| |
| } |
| |
| /** |
| * Delete the revisions, the RCML source and the collection if the latter is empty. |
| * @return boolean true, if the file was deleted |
| */ |
| public synchronized boolean delete() { |
| try { |
| deleteRevisions(); |
| SourceUtil.delete(getRcmlSourceUri(), this.manager); |
| SourceUtil.deleteEmptyCollections(getRcmlSourceUri(), this.manager); |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| return true; |
| } |
| |
| /** |
| * delete the revisions |
| * @throws RevisionControlException when somthing went wrong |
| */ |
| public synchronized void deleteRevisions() throws RevisionControlException { |
| try { |
| String[] times = getBackupsTime(); |
| for (int i = 0; i < times.length; i++) { |
| long time = new Long(times[i]).longValue(); |
| deleteBackup(this.contentSourceUri, time); |
| deleteBackup(this.metaSourceUri, time); |
| } |
| this.entries.clear(); |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| } |
| |
| public synchronized void copyFrom(Node node, Node otherNode) throws RevisionControlException { |
| |
| SourceNode otherSourceNode = (SourceNode) otherNode; |
| SourceNode sourceNode = (SourceNode) node; |
| SourceNodeRCML otherRcml = (SourceNodeRCML) ((SourceNode) otherNode).getRcml(); |
| |
| try { |
| |
| Vector backupEntries = otherRcml.getBackupEntries(); |
| for (Iterator i = backupEntries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| long time = entry.getTime(); |
| String otherContentUri = otherRcml.getBackupSourceUri(otherSourceNode |
| .getContentSource(), time); |
| String thisContentUri = this |
| .getBackupSourceUri(sourceNode.getContentSource(), time); |
| SourceUtil.copy(this.manager, otherContentUri, thisContentUri); |
| |
| String otherMetaUri = otherRcml.getBackupSourceUri(otherSourceNode.getMetaSource(), |
| time); |
| String thisMetaUri = this.getBackupSourceUri(sourceNode.getMetaSource(), time); |
| SourceUtil.copy(this.manager, otherMetaUri, thisMetaUri); |
| } |
| |
| this.entries = new Vector(); |
| Vector otherEntries = otherRcml.getEntries(); |
| for (Iterator i = otherEntries.iterator(); i.hasNext();) { |
| RCMLEntry entry = (RCMLEntry) i.next(); |
| RCMLEntry newEntry = null; |
| switch (entry.getType()) { |
| case co: |
| newEntry = new CheckOutEntry(entry.getSessionId(), entry.getIdentity(), entry |
| .getTime()); |
| break; |
| case ci: |
| CheckInEntry ciEntry = (CheckInEntry) entry; |
| newEntry = new CheckInEntry(ciEntry.getSessionId(), ciEntry.getIdentity(), |
| ciEntry.getTime(), ciEntry.getVersion(), ciEntry.hasBackup()); |
| break; |
| } |
| this.entries.add(newEntry); |
| } |
| |
| write(); |
| } catch (Exception e) { |
| throw new RevisionControlException(e); |
| } |
| } |
| |
| public synchronized boolean isCheckedOut() throws RevisionControlException { |
| RCMLEntry entry = getLatestEntry(); |
| return entry != null && entry.getType() == RCML.co; |
| } |
| |
| public synchronized void checkIn(Node node, boolean backup, boolean newVersion) |
| throws RevisionControlException { |
| long time = new Date().getTime(); |
| |
| if (backup) { |
| makeBackup(time); |
| } |
| |
| checkOutIn(node, RCML.ci, time, backup, newVersion, false); |
| pruneEntries(); |
| write(); |
| } |
| |
| public synchronized void checkOut(Node node) throws RevisionControlException { |
| checkOut(node, true); |
| } |
| |
| public synchronized void checkOut(Node node, boolean restrictedToSession) |
| throws RevisionControlException { |
| checkOutIn(node, RCML.co, new Date().getTime(), false, false, restrictedToSession); |
| write(); |
| } |
| |
| public boolean isCheckedOutBySession(Session session) throws RevisionControlException { |
| Vector entries = getEntries(); |
| if (entries.size() > 0) { |
| RCMLEntry entry = (RCMLEntry) entries.get(0); |
| String otherSessionId = entry.getSessionId(); |
| if (entry.getType() == co) { |
| // not restricted to session |
| if (otherSessionId.equals(ALL_SESSIONS)) { |
| String otherUserId = entry.getIdentity(); |
| String userId = session.getIdentity().getUser().getId(); |
| return userId.equals(otherUserId); |
| } |
| // restricted to session |
| if (otherSessionId.equals(session.getId())) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| } |