/* * eXist Open Source Native XML Database * Copyright (C) 2005-2011 The eXist-db Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * $Id: Restore.java 15109 2011-08-09 13:03:09Z deliriumsky $ */ package org.exist.backup.restore; import java.io.IOException; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Observable; import java.util.Stack; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Namespaces; import org.exist.backup.BackupDescriptor; import org.exist.backup.restore.listener.RestoreListener; import org.exist.dom.persistent.DocumentTypeImpl; import org.exist.security.ACLPermission.ACE_ACCESS_TYPE; import org.exist.security.ACLPermission.ACE_TARGET; import org.exist.security.SecurityManager; import org.exist.util.EXistInputSource; import org.exist.xmldb.CollectionImpl; import org.exist.xmldb.CollectionManagementServiceImpl; import org.exist.xmldb.EXistResource; import org.exist.xmldb.XmldbURI; import org.exist.xquery.XPathException; import org.exist.xquery.util.URIUtils; import org.exist.xquery.value.DateTimeValue; import org.w3c.dom.DocumentType; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xmldb.api.DatabaseManager; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Resource; import org.xmldb.api.base.XMLDBException; import org.xmldb.api.modules.CollectionManagementService; /** * Handler for parsing __contents.xml__ files during * restoration of a db backup * * @author Adam Retter <adam@exist-db.org> */ public class RestoreHandler extends DefaultHandler { private final static Logger LOG = LogManager.getLogger(RestoreHandler.class); private final static SAXParserFactory saxFactory = SAXParserFactory.newInstance(); static { saxFactory.setNamespaceAware(true); saxFactory.setValidating(false); } private static final int STRICT_URI_VERSION = 1; private final RestoreListener listener; private final String dbBaseUri; private final String dbUsername; private final String dbPassword; private final BackupDescriptor descriptor; //handler state private int version = 0; private CollectionImpl currentCollection; private Stack<DeferredPermission> deferredPermissions = new Stack<DeferredPermission>(); public RestoreHandler(final RestoreListener listener, final String dbBaseUri, final String dbUsername, final String dbPassword, final BackupDescriptor descriptor) { this.listener = listener; this.dbBaseUri = dbBaseUri; this.dbUsername = dbUsername; this.dbPassword = dbPassword; this.descriptor = descriptor; } @Override public void startDocument() throws SAXException { listener.setCurrentBackup(descriptor.getSymbolicPath()); } /** * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ @Override public void startElement(final String namespaceURI, final String localName, final String qName, final Attributes atts) throws SAXException { //only process entries in the exist namespace if(namespaceURI != null && !namespaceURI.equals(Namespaces.EXIST_NS)) { return; } if("collection".equals(localName) || "resource".equals(localName)) { final DeferredPermission df; if("collection".equals(localName)) { df = restoreCollectionEntry(atts); } else { df = restoreResourceEntry(atts); } deferredPermissions.push(df); } else if("subcollection".equals(localName)) { restoreSubCollectionEntry(atts); } else if("deleted".equals(localName)) { restoreDeletedEntry(atts); } else if("ace".equals(localName)) { addACEToDeferredPermissions(atts); } } @Override public void endElement(final String namespaceURI, final String localName, final String qName) throws SAXException { if(namespaceURI.equals(Namespaces.EXIST_NS) && ("collection".equals(localName) || "resource".equals(localName))) { setDeferredPermissions(); } super.endElement(namespaceURI, localName, qName); } private String getAttr(final Attributes atts, final String name, final String fallback) { final String value = atts.getValue(name); if(value == null) { return fallback; } return value; } private DeferredPermission restoreCollectionEntry(final Attributes atts) throws SAXException { final String name = atts.getValue("name"); if(name == null) { throw new SAXException("Collection requires a name attribute"); } final String owner = getAttr(atts, "owner", SecurityManager.SYSTEM); final String group = getAttr(atts, "group", SecurityManager.DBA_GROUP); final String mode = getAttr(atts, "mode", "644"); final String created = atts.getValue("created"); final String strVersion = atts.getValue("version"); if(strVersion != null) { try { this.version = Integer.parseInt(strVersion); } catch(final NumberFormatException nfe) { final String msg = "Could not parse version number for Collection '" + name + "', defaulting to version 0"; listener.warn(msg); LOG.warn(msg); this.version = 0; } } try { listener.createCollection(name); final XmldbURI collUri; if(version >= STRICT_URI_VERSION) { collUri = XmldbURI.create(name); } else { try { collUri = URIUtils.encodeXmldbUriFor(name); } catch(final URISyntaxException e) { listener.warn("Could not parse document name into a URI: " + e.getMessage()); return new SkippedEntryDeferredPermission(); } } currentCollection = mkcol(collUri, getDateFromXSDateTimeStringForItem(created, name)); listener.setCurrentCollection(name); if(currentCollection == null) { throw new SAXException("Collection not found: " + collUri); } final DeferredPermission deferredPermission; if(name.startsWith(XmldbURI.SYSTEM_COLLECTION)) { //prevents restore of a backup from changing System collection ownership deferredPermission = new CollectionDeferredPermission(listener, currentCollection, SecurityManager.SYSTEM, SecurityManager.DBA_GROUP, Integer.parseInt(mode, 8)); } else { deferredPermission = new CollectionDeferredPermission(listener, currentCollection, owner, group, Integer.parseInt(mode, 8)); } return deferredPermission; } catch(final Exception e) { final String msg = "An unrecoverable error occurred while restoring\ncollection '" + name + "'. " + "Aborting restore!"; LOG.error(msg, e); listener.warn(msg); throw new SAXException(e.getMessage(), e); } } private void restoreSubCollectionEntry(final Attributes atts) throws SAXException { final String name; if(atts.getValue("filename") != null) { name = atts.getValue("filename"); } else { name = atts.getValue("name"); } //exclude /db/system collection and sub-collections, as these have already been restored try { final String currentCollectionName = currentCollection.getName(); if(("/db".equals(currentCollectionName) && "system".equals(name)) || ("/db/system".equals(currentCollectionName) && "security".equals(name))) { return; } } catch(final XMLDBException xe) { throw new RuntimeException(xe.getMessage(), xe); } //parse the sub-collection descriptor and restore final BackupDescriptor subDescriptor = descriptor.getChildBackupDescriptor(name); if(subDescriptor != null) { final SAXParser sax; try { sax = saxFactory.newSAXParser(); final XMLReader reader = sax.getXMLReader(); final EXistInputSource is = subDescriptor.getInputSource(); is.setEncoding( "UTF-8" ); final RestoreHandler handler = new RestoreHandler(listener, dbBaseUri, dbUsername, dbPassword, subDescriptor); reader.setContentHandler(handler); reader.parse(is); } catch(final ParserConfigurationException pce) { listener.error("Could not initalise SAXParser for processing sub-collection: " + descriptor.getSymbolicPath(name, false)); } catch(final IOException ioe) { listener.error("Could not read sub-collection for processing: " + ioe.getMessage()); } catch(final SAXException se) { listener.error("SAX exception while reading sub-collection " + subDescriptor.getSymbolicPath() + " for processing: " + se.getMessage()); } } else { listener.error("Collection " + descriptor.getSymbolicPath(name, false) + " does not exist or is not readable."); } } private DeferredPermission restoreResourceEntry(final Attributes atts) throws SAXException { final String skip = atts.getValue( "skip" ); //dont process entries which should be skipped if(skip != null && !"no".equals(skip)) { return new SkippedEntryDeferredPermission(); } final String name = atts.getValue("name"); if(name == null) { throw new SAXException("Resource requires a name attribute"); } //triggers should NOT be disabled, because it do used by the system tasks (like security manager) //UNDERSTAND: split triggers: user & system //current.setTriggersEnabled(false); /* try { if(currentCollection.getName().equals("/db/system") && name.equals("users.xml") && currentCollection.getChildCollection("security") != null) { listener.warn("Skipped resource '" + name + "'\nfrom file '" + descriptor.getSymbolicPath(name, false) + "'."); return new SkippedEntryDeferredPermission(); } } catch(XMLDBException xe) { LOG.error(xe.getMessage(), xe); listener.error(xe.getMessage()); return new SkippedEntryDeferredPermission(); }*/ final String type; if(atts.getValue("type") != null) { type = atts.getValue("type"); } else { type = "XMLResource"; } final String owner = getAttr(atts, "owner", SecurityManager.SYSTEM); final String group = getAttr(atts, "group", SecurityManager.DBA_GROUP); final String perms = getAttr(atts, "mode", "644"); final String filename; if(atts.getValue("filename") != null) { filename = atts.getValue("filename"); } else { filename = name; } final String mimetype = atts.getValue("mimetype"); final String created = atts.getValue("created"); final String modified = atts.getValue("modified"); final String publicid = atts.getValue("publicid"); final String systemid = atts.getValue("systemid"); final String namedoctype = atts.getValue("namedoctype"); final XmldbURI docUri; if(version >= STRICT_URI_VERSION) { docUri = XmldbURI.create(name); } else { try { docUri = URIUtils.encodeXmldbUriFor(name); } catch(final URISyntaxException e) { final String msg = "Could not parse document name into a URI: " + e.getMessage(); listener.error(msg); LOG.error(msg, e); return new SkippedEntryDeferredPermission(); } } final EXistInputSource is = descriptor.getInputSource(filename); if(is == null) { final String msg = "Failed to restore resource '" + name + "'\nfrom file '" + descriptor.getSymbolicPath( name, false ) + "'.\nReason: Unable to obtain its EXistInputSource"; listener.warn(msg); return new SkippedEntryDeferredPermission(); } try { listener.setCurrentResource(name); if(currentCollection instanceof Observable) { listener.observe((Observable)currentCollection); } Resource res = currentCollection.createResource(docUri.toString(), type); if(mimetype != null) { ((EXistResource)res).setMimeType(mimetype); } if(is.getByteStreamLength() > 0) { res.setContent(is); } else { if("BinaryResource".equals(type)) { res.setContent(""); } else { res = null; } } // Restoring name if(res == null) { listener.warn("Failed to restore resource '" + name + "'\nfrom file '" + descriptor.getSymbolicPath(name, false) + "'. The resource is empty."); return new SkippedEntryDeferredPermission(); } else { Date date_created = null; Date date_modified = null; if(created != null) { try { date_created = (new DateTimeValue(created)).getDate(); } catch(final XPathException xpe) { listener.warn("Illegal creation date. Ignoring date..."); } } if(modified != null) { try { date_modified = (Date) (new DateTimeValue(modified)).getDate(); } catch(final XPathException xpe) { listener.warn("Illegal modification date. Ignoring date..."); } } currentCollection.storeResource(res, date_created, date_modified); if((publicid != null) || (systemid != null)) { final DocumentType doctype = new DocumentTypeImpl(namedoctype, publicid, systemid); try { ((EXistResource) res).setDocType(doctype); } catch(final XMLDBException e1) { LOG.error(e1.getMessage(), e1); } } final DeferredPermission deferredPermission; if(name.startsWith(XmldbURI.SYSTEM_COLLECTION)) { //prevents restore of a backup from changing system collection resource ownership deferredPermission = new ResourceDeferredPermission(listener, res, SecurityManager.SYSTEM, SecurityManager.DBA_GROUP, Integer.parseInt(perms, 8)); } else { deferredPermission = new ResourceDeferredPermission(listener, res, owner, group, Integer.parseInt(perms, 8)); } listener.restored(name); return deferredPermission; } } catch(final Exception e) { listener.warn("Failed to restore resource '" + name + "'\nfrom file '" + descriptor.getSymbolicPath(name, false) + "'.\nReason: " + e.getMessage()); LOG.error(e.getMessage(), e); return new SkippedEntryDeferredPermission(); } finally { is.close(); } } private void restoreDeletedEntry(final Attributes atts) { final String name = atts.getValue("name"); final String type = atts.getValue("type"); if("collection".equals(type)) { try { final Collection child = currentCollection.getChildCollection(name); if(child != null) { currentCollection.setTriggersEnabled(false); final CollectionManagementService cmgt = (CollectionManagementService)currentCollection.getService("CollectionManagementService", "1.0"); cmgt.removeCollection(name); currentCollection.setTriggersEnabled(true); } } catch(final XMLDBException e) { listener.warn("Failed to remove deleted collection: " + name + ": " + e.getMessage()); } } else if("resource".equals(type)) { try { final Resource resource = currentCollection.getResource(name); if(resource != null) { currentCollection.setTriggersEnabled(false); currentCollection.removeResource(resource); currentCollection.setTriggersEnabled(true); } } catch(final XMLDBException e) { listener.warn("Failed to remove deleted resource: " + name + ": " + e.getMessage()); } } } private void addACEToDeferredPermissions(final Attributes atts) { final int index = Integer.parseInt(atts.getValue("index")); final ACE_TARGET target = ACE_TARGET.valueOf(atts.getValue("target")); final String who = atts.getValue("who"); final ACE_ACCESS_TYPE access_type = ACE_ACCESS_TYPE.valueOf(atts.getValue("access_type")); final int mode = Integer.parseInt(atts.getValue("mode"), 8); deferredPermissions.peek().addACE(index, target, who, access_type, mode); } private void setDeferredPermissions() { final DeferredPermission deferredPermission = deferredPermissions.pop(); deferredPermission.apply(); } private Date getDateFromXSDateTimeStringForItem(final String strXSDateTime, final String itemName) { Date date_created = null; if(strXSDateTime != null) { try { date_created = new DateTimeValue(strXSDateTime).getDate(); } catch(final XPathException e2) { } } if(date_created == null) { final String msg = "Could not parse created date '" + strXSDateTime + "' from backup for: '" + itemName + "', using current time!"; listener.error(msg); LOG.error(msg); date_created = Calendar.getInstance().getTime(); } return date_created; } private CollectionImpl mkcol(final XmldbURI collPath, final Date created) throws XMLDBException, URISyntaxException { final XmldbURI[] allSegments = collPath.getPathSegments(); final XmldbURI[] segments = Arrays.copyOfRange(allSegments, 1, allSegments.length); //drop the first 'db' segment final XmldbURI dbUri; if(!dbBaseUri.endsWith(XmldbURI.ROOT_COLLECTION)) { dbUri = XmldbURI.xmldbUriFor(dbBaseUri + XmldbURI.ROOT_COLLECTION); } else { dbUri = XmldbURI.xmldbUriFor(dbBaseUri); } CollectionImpl current = (CollectionImpl)DatabaseManager.getCollection(dbUri.toString(), dbUsername, dbPassword); XmldbURI p = XmldbURI.ROOT_COLLECTION_URI; for(final XmldbURI segment : segments) { p = p.append(segment); final XmldbURI xmldbURI = dbUri.resolveCollectionPath(p); CollectionImpl c = (CollectionImpl)DatabaseManager.getCollection(xmldbURI.toString(), dbUsername, dbPassword); if(c == null) { current.setTriggersEnabled(false); final CollectionManagementServiceImpl mgtService = (CollectionManagementServiceImpl)current.getService("CollectionManagementService", "1.0"); c = (CollectionImpl)mgtService.createCollection(segment, created); current.setTriggersEnabled(true); } current = c; } return current; } }