Index: ivy.xml =================================================================== --- ivy.xml (revision 19862) +++ ivy.xml (working copy) @@ -45,7 +45,6 @@ - Index: main/plugin-web/i18n/messages_fr.xml =================================================================== --- main/plugin-web/i18n/messages_fr.xml (revision 19862) +++ main/plugin-web/i18n/messages_fr.xml (working copy) @@ -326,6 +326,8 @@ Statistiques du site Statistiques globales Supprimer + Importer un site + Exporter un site Ouvrir dans le CMS Nouveau site Configurer ... @@ -449,6 +451,29 @@ Explorateur de ressources Pages + Importer un site... + Sélectionnez le fichier compressé au format ZIP d'un site Ametys à importer : + Fichier ZIP + Veuillez sélectionner le fichier zip à charger + Parcourir + Fichier de type incorrect + Vous devez choisir un site Ametys au format ZIP + Ok + Annuler + Téléchargement... + Le fichier est en train d'être envoyé sur le serveur. Veuillez patienter. + Fichier trop grand + Le fichier chargé est trop grand. La taille maximale autorisée se règle dans l'espace 'Configuration' + Site existant + Un site du même nom que celui envoyé sur le serveur existe déjà. + Erreur inconnue + Une erreur inconnue a empêché l'import du site + Import du site en cours. Veuillez patienter... + Import du site + L'import du site est terminé. + Erreur lors de l'import du site + Une erreur est survenue lors de l'import du site. Le site n'a pas pu être correctement importé. + Index: main/plugin-web/resources/js/org/ametys/web/administration/Sites.i18n.js =================================================================== --- main/plugin-web/resources/js/org/ametys/web/administration/Sites.i18n.js (revision 19862) +++ main/plugin-web/resources/js/org/ametys/web/administration/Sites.i18n.js (working copy) @@ -42,6 +42,8 @@ this._handle.addAction("", getPluginResourcesUrl(this.pluginName) + "/img/administrator/sites/clear_cache.png", org.ametys.web.administration.site.ClearCache);// 8 this._handle.addAction("", getPluginResourcesUrl(this.pluginName) + "/img/administrator/sites/clear_cache_all.png", org.ametys.web.administration.site.ClearCacheAll);// 9 this._handle.addAction("", getPluginResourcesUrl(this.pluginName) + "/img/administrator/sites/delete.png", org.ametys.web.administration.site.Delete);// 10 + this._handle.addAction("", getPluginResourcesUrl(this.pluginName) + "/img/administrator/sites/import.png", org.ametys.web.administration.site.Import);// 11 + this._handle.addAction("", getPluginResourcesUrl(this.pluginName) + "/img/administrator/sites/export.png", org.ametys.web.administration.site.Export);// 12 // Quit action this._handle.addAction("", getPluginResourcesUrl('core') + '/img/administrator/config/quit.png', org.ametys.web.administration.site.goBack);// 9 @@ -56,6 +58,7 @@ this._handle.hideElt(6); this._handle.hideElt(8); this._handle.hideElt(10); + this._handle.hideElt(12); this._rightPanel = new org.ametys.HtmlContainer({ region:'east', @@ -154,6 +157,7 @@ this._handle.hideElt(6); this._handle.hideElt(8); this._handle.hideElt(10); + this._handle.hideElt(12); } else { @@ -164,6 +168,7 @@ this._handle.showElt(6); this._handle.showElt(8); this._handle.showElt(10); + this._handle.showElt(12); } } @@ -309,6 +314,22 @@ parentNode.select(); } } + +/**-----------------------------------------------------------*/ + +org.ametys.web.administration.site.Export = function () +{ + var node = org.ametys.web.administration.Site._sites.getSelectionModel().getSelectedNode(); + var url = getPluginDirectUrl('web') + "/sites/export-site/" + node.attributes.name + ".zip?id=" + node.id; + window.open(url); +} + +/**-----------------------------------------------------------*/ +org.ametys.web.administration.site.Import = function () +{ + org.ametys.web.administration.site.Import.act(); +} + /**-----------------------------------------------------------*/ org.ametys.web.administration.site.BuildPreview = function() { @@ -925,3 +946,210 @@ org.ametys.web.administration.Site._dv.getStore().load(); org.ametys.web.administration.Site._tree.getRootNode().reload(); } + +//------------------------------------------------------- +// SITE IMPORT +//------------------------------------------------------- +org.ametys.web.administration.site.Import._initialized = false; + +org.ametys.web.administration.site.Import.act = function () +{ + if (!org.ametys.web.administration.site.Import.delayedInitialize()) + return; + + var node = org.ametys.web.administration.Site._sites.getSelectionModel().getSelectedNode(); + org.ametys.web.administration.site.Import._parentId = node && node.id || undefined; + org.ametys.web.administration.site.Import.box.show(); + org.ametys.web.administration.site.Import._form.getForm().findField('importfile').reset(); +} + +org.ametys.web.administration.site.Import.delayedInitialize = function () +{ + if (org.ametys.web.administration.site.Import._initialized) + return true; + + org.ametys.web.administration.site.Import._form = new Ext.form.FormPanel({ + id : 'import-site', + border :false, + style: { + paddingTop: '8px' + }, + + fileUpload: true, + labelWidth :80, + defaultType :'textfield', + + items:[ + new org.ametys.form.FileUploadField({ + emptyText: "", + fieldLabel :"", + buttonText: "", + name :'importfile', + width :280, + listeners: {'fileselected': org.ametys.web.administration.site.Import._select}, + msgTarget: 'side', + }) + ] + }); + + org.ametys.web.administration.site.Import.box = new org.ametys.DialogBox({ + + title :"", + icon : getPluginResourcesUrl("web") + "/img/administrator/sites/import_site_16.png", + + cls: 'text-dialog', + bodyStyle: { + backgroundColor: '#FFFFFF' + }, + width:450, + height:150, + + items : [ new org.ametys.HtmlContainer ({cls: 'dialog-text-hint', html: ""}), + org.ametys.web.administration.site.Import._form ], + + defaultButton: org.ametys.web.administration.site.Import._form.getForm().findField('importfile'), + closeAction: 'hide', + buttons : [ { + text :"", + disabled: true, + handler : org.ametys.web.administration.site.Import.ok + }, { + text :"", + handler : org.ametys.web.administration.site.Import.cancel + } + ] + }); + + org.ametys.web.administration.site.Import._initialized = true; + return true; +} + +org.ametys.web.administration.site.Import.cancel = function() +{ + org.ametys.web.administration.site.Import.box.hide(); +} + + +org.ametys.web.administration.site.Import._filter = function(filename) +{ + return /\.zip$/i.test(filename); +} + +org.ametys.web.administration.site.Import._select = function(fileField, name) +{ + var disabled = !org.ametys.web.administration.site.Import._filter(name); + org.ametys.web.administration.site.Import.box.buttons[0].setDisabled(disabled); + if (disabled) + { + new org.ametys.msg.ErrorDialog("", + "", + name, + "org.ametys.web.administration.site.Import._select"); + + fileField.reset(); + } +} + +org.ametys.web.administration.site.Import.ok = function() +{ + org.ametys.web.administration.site.Import.box.hide(); + org.ametys.web.administration.site.Import._upload(); +} + +org.ametys.web.administration.site.Import._upload = function () +{ + org.ametys.web.administration.site.Import._form.getForm().submit({ + url : getPluginDirectUrl('web') + "/sites/import-site/upload", + + waitTitle: "", + waitMsg: "", + + success: org.ametys.web.administration.site.Import._submitSuccess, + failure: org.ametys.web.administration.site.Import._submitFailure + }); +} + +org.ametys.web.administration.site.Import._submitSuccess = function(form, action) +{ + org.ametys.web.administration.site.Import._doImport(action.result.siteName, action.result.tmpFilePath); +} + +org.ametys.web.administration.site.Import._doImport = function(siteName, tmpFilePath) +{ + org.ametys.web.administration.site.Import._mask = org.ametys.msg.Mask(null, ""); // FIXME msg / el + + var params = {'siteName': siteName, 'tmpFilePath': tmpFilePath, 'deleteFile': 'true', 'parentId' : org.ametys.web.administration.site.Import._parentId}; + var serverMessage = new org.ametys.servercomm.ServerMessage('web', '/sites/import-site', params, org.ametys.servercomm.ServerComm.PRIORITY_MAJOR, org.ametys.web.administration.site.Import._doImportCallback, this, params, "xml"); + var response = org.ametys.servercomm.ServerComm.getInstance().send(serverMessage); +} + +org.ametys.web.administration.site.Import._doImportCallback = function(response, args) +{ + org.ametys.web.administration.site.Import._mask.hide(); + + if (org.ametys.servercomm.ServerComm.handleBadResponse("", response, 'org.ametys.web.administration.site.Import._doImport')) + { + return; + } + + var success = response.selectSingleNode('ActionResult/success')[org.ametys.servercomm.ServerComm.xmlTextContent]; + + if (success === "true") + { + Ext.Msg.show({ + title: " (" + args.siteName + ")", + msg: "", + buttons: Ext.Msg.OK, + icon: Ext.Msg.INFO + }); + + // Reload widget and select the newly imported site + var node = org.ametys.web.administration.Site._sites.getNodeById(args.parentId) || org.ametys.web.administration.Site._sites.getRootNode().childNodes[0]; + if (node) + { + node.reload(function() { + var siteNode = node.findChild('name', args.siteName); + if (siteNode) + { + siteNode.select(); + console.log(siteNode.attributes.text) + } + }); + + } + } + else + { + var error = response.selectSingleNode('ActionResult/error')[org.ametys.servercomm.ServerComm.xmlTextContent]; + new org.ametys.msg.ErrorDialog("", + "", + error, + "org.ametys.web.administration.site.Import._doImport"); + } + +} + +org.ametys.web.administration.site.Import._submitFailure = function(form, action) +{ + if (action.result.error == "rejected") + { + new org.ametys.msg.ErrorDialog("", + "", + "", + "org.ametys.web.administration.site.Import._submitFailure"); + } + if (action.result.error == "site-existing") + { + new org.ametys.msg.ErrorDialog("", + "", + "", + "org.ametys.web.administration.site.Import._submitFailure"); + } + else + { + new org.ametys.msg.ErrorDialog("", + "", + action.result.error.message, + "org.ametys.web.administration.site.Import._submitFailure"); + } +} Index: main/plugin-web/plugin.xml =================================================================== --- main/plugin-web/plugin.xml (revision 19862) +++ main/plugin-web/plugin.xml (working copy) @@ -8580,4 +8580,13 @@ + + + + + + + Index: main/plugin-web/src/org/ametys/web/site/io/importers/SiteNodeImporter.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/importers/SiteNodeImporter.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/importers/SiteNodeImporter.java (revision 0) @@ -0,0 +1,177 @@ +package org.ametys.web.site.io.importers; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.handlers.ImportAmetysObjectHandler; +import org.ametys.cms.io.importers.ImportReferenceTracker; +import org.ametys.cms.io.importers.JcrImporter; +import org.ametys.cms.repository.CloneComponent; +import org.ametys.plugins.repository.AmetysObject; +import org.ametys.plugins.repository.AmetysObjectResolver; +import org.ametys.plugins.repository.AmetysRepositoryException; +import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; +import org.ametys.plugins.repository.UnknownAmetysObjectException; +import org.ametys.web.repository.site.Site; +import org.ametys.web.repository.site.SiteManager; + +/** + * Site node importer using the JCR Import. + */ +public class SiteNodeImporter extends JcrImporter +{ + /** Avalon role. */ + public static final String ROLE = SiteNodeImporter.class.getName(); + + /** Site node import input stream property */ + public static final String INPUT_STREAM_PROPERTY = "inputStream"; + + /** The Ametys object resolver */ + protected AmetysObjectResolver _resolver; + + /** The site manager */ + protected SiteManager _siteManager; + + /** The clone component. */ + protected CloneComponent _cloneComponent; + + @Override + public void service(ServiceManager manager) throws ServiceException + { + super.service(manager); + _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); + _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); + _cloneComponent = (CloneComponent) manager.lookup(CloneComponent.ROLE); + } + + /** + * Import a site node from an inputstream. + * @param parentSiteId + * @param siteName + * @param importSession The session in which the import will be done. + * @param defaultSession + * @param inputStream + * @param workflowIdMapping + * @param referenceTracker + * @return the importWorkflowHandler + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public ImportAmetysObjectHandler doImport(String parentSiteId, String siteName, Session importSession, Session defaultSession, InputStream inputStream, Map workflowIdMapping, ImportReferenceTracker referenceTracker) throws RepositoryException, SAXException, IOException + { + Node siteParentNode = _getSiteParentNode(parentSiteId, siteName, importSession, defaultSession); + + // Get the import handler + int uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW; // An exception will be thrown if there is an identifier collision. + ImportAmetysObjectHandler importHandlerProxy = new ImportAmetysObjectHandler(siteParentNode, referenceTracker, workflowIdMapping); + + // Performing the import. + importNode(siteParentNode, uuidBehavior, inputStream, importHandlerProxy); + + return importHandlerProxy; + } + + /** + * Get (and create if necessary) the parent node of a site in the JCR hierarchy. + * @param parentSiteId the ametys id of the parent site. Null if the site has no parent. + * @param siteName Name of the site to import. + * @param importSession The session in which the import is done + * @param defaultSession Session bound to the default workspace. + * @return The parent node of the site node. + * @throws RepositoryException + */ + private Node _getSiteParentNode(String parentSiteId, String siteName, Session importSession, Session defaultSession) throws RepositoryException + { + if (importSession.getWorkspace().getName().equals(defaultSession.getWorkspace().getName())) + { + // Default workspace. + Site parentSite = _getSite(parentSiteId, importSession); + return _getSiteParentNode(parentSite, siteName, importSession); + } + else + { + // Other workspaces (should be 'archives'): + // Import must be done is the default workspace first. So the site + // must exist in the default workspace while importing into another + // workspace. + if (_siteManager.hasSite(siteName)) + { + Node siteNodeDefaultWS = _siteManager.getSite(siteName).getNode(); + return _cloneComponent.cloneAncestorsAndPreserveUUID(siteNodeDefaultWS, importSession); + } + } + + String errorMsg = "Unable to get the parent node of the site node to import."; + getLogger().error(errorMsg); + throw new IllegalStateException(errorMsg); + } + + /** + * Get (and create if necessary) the parent node of a site in the JCR hierarchy. + * @param parentSite Parent site of the site to import. + * @param siteName Name of the site to import. + * @param session The session used to retrieve and create nodes if the parent site is null. + * @return + * @throws RepositoryException + */ + private Node _getSiteParentNode(Site parentSite, String siteName, Session session) throws RepositoryException + { + ModifiableTraversableAmetysObject parentRootSites = null; + + if (parentSite == null) + { + // Retrieves the root Ametys object where "ametys:site" are created within the jcrSession + // We currently use a workaround, see REPOSITORY-212 + // Must do _resolver.resolve(SiteManager.ROOT_SITES_PATH, jcrSession); when the API will allow it. + Node rootSitesNode = session.getRootNode().getNode(AmetysObjectResolver.ROOT_REPO + SiteManager.ROOT_SITES_PATH); + parentRootSites = _resolver.resolve(rootSitesNode, false); + } + else + { + // Retrieves the Ametys object where sub-sites are created for parentSite. + if (!parentSite.hasChild(SiteManager.ROOT_SITES)) + { + parentSite.createChild(SiteManager.ROOT_SITES, "ametys:sites"); + } + parentRootSites = parentSite.getChild(SiteManager.ROOT_SITES); + } + + // Create the child site, keep a reference to its direct parent node and + // remove the node of the site. + Node siteNode = ((Site) parentRootSites.createChild(siteName, "ametys:site")).getNode(); + Node parentNode = siteNode.getParent(); + siteNode.remove(); + + return parentNode; + } + + /** + * Retrieves a {@link Site} by its id. + * @param jcrSession + * @throws RepositoryException + * @throws AmetysRepositoryException + * @throws UnknownAmetysObjectException + * @Return the {@link Site} instance, or null if the retrieved {@link AmetysObject} is not a {@link Site} + */ + private Site _getSite(String id, Session jcrSession) throws RepositoryException + { + if (id == null) + { + return null; + } + + AmetysObject ao = _resolver.resolveById(id, jcrSession); + return ao instanceof Site ? (Site) ao : null; + } +} Index: main/plugin-web/src/org/ametys/web/site/io/importers/SiteImporter.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/importers/SiteImporter.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/importers/SiteImporter.java (revision 0) @@ -0,0 +1,347 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed 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. + */ +package org.ametys.web.site.io.importers; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.avalon.framework.component.Component; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.avalon.framework.service.Serviceable; +import org.apache.commons.lang.BooleanUtils; +import org.xml.sax.SAXException; + +import org.ametys.cms.content.archive.ArchiveConstants; +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOUtils; +import org.ametys.cms.io.handlers.ImportAmetysObjectHandler; +import org.ametys.cms.io.handlers.ImportWorkflowHandler; +import org.ametys.cms.io.importers.AmetysImporter; +import org.ametys.cms.io.importers.BinaryPropertiesImporter; +import org.ametys.cms.io.importers.ImportReferenceTracker; +import org.ametys.cms.io.importers.ResourcesImporter; +import org.ametys.cms.io.importers.ResourcesImporter.BinaryResourceImportData; +import org.ametys.cms.io.importers.ResourcesImporter.ResourceNodeImportData; +import org.ametys.cms.io.importers.WorkflowImporter; +import org.ametys.plugins.repository.AmetysObjectResolver; +import org.ametys.plugins.repository.provider.AbstractRepository; +import org.ametys.plugins.workflow.Workflow; +import org.ametys.plugins.workflow.store.AmetysStep; +import org.ametys.web.WebConstants; + +import com.opensymphony.workflow.spi.Step; + +/** + * Site importer + */ +public class SiteImporter extends AmetysImporter implements Component, Serviceable +{ + /** Avalon role. */ + public static final String ROLE = SiteImporter.class.getName(); + + // Key used to configure the importers + /** configuration key of the site node importer */ + public static final String SITE_NODE_PROPERTIES = "siteNode"; + /** configuration key of the workflow importer */ + public static final String WORKFLOWS_PROPERTIES = "workflows"; + /** configuration key of the resource importer */ + public static final String RESOURCES_PROPERTIES = "resources"; + /** configuration key of the binary properties importer */ + public static final String BINARY_PROPERTIES = "binary"; + + /** JCR Repository */ + protected Repository _repository; + + /** The workflow */ + protected Workflow _workflow; + + /** The workflow importer */ + protected WorkflowImporter _workflowImporter; + + /** The site node importer */ + protected SiteNodeImporter _siteNodeImporter; + + /** The binary properties importer */ + protected BinaryPropertiesImporter _binaryPropertiesImporter; + + /** The ametys resources exporter */ + protected ResourcesImporter _resourcesImporter; + + /** Map containing the properties for each exporter used during the global import */ + protected Map _importerProperties = new HashMap(); + + @Override + public void service(ServiceManager manager) throws ServiceException + { + _repository = (Repository) manager.lookup(AbstractRepository.ROLE); + _workflowImporter = (WorkflowImporter) manager.lookup(WorkflowImporter.ROLE); + _siteNodeImporter = (SiteNodeImporter) manager.lookup(SiteNodeImporter.ROLE); + _binaryPropertiesImporter = (BinaryPropertiesImporter) manager.lookup(BinaryPropertiesImporter.ROLE); + _resourcesImporter = (ResourcesImporter) manager.lookup(ResourcesImporter.ROLE); + _workflow = (Workflow) manager.lookup(Workflow.ROLE); + } + + + /** + * Proceed the import of a site into the default workspace. + * @param parentSiteId + * @param siteName + * @param importerProperties + * @return The imported node + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public Node doImport(String parentSiteId, String siteName, Map importerProperties) throws RepositoryException, SAXException, IOException + { + Session session = null; + + try + { + session = _repository.login(); + return _doImportInternal(parentSiteId, siteName, importerProperties, session, session); + } + finally + { + if (session != null) + { + session.logout(); + } + } + } + + /** + * Proceed the import of a site into the archive workspace. + * @param parentSiteId + * @param siteName + * @param importerProperties + * @return The imported node + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public Node doImportArchives(String parentSiteId, String siteName, Map importerProperties) throws RepositoryException, SAXException, IOException + { + Session defaultSession = null; + Session archiveSession = null; + + try + { + archiveSession = _getArchiveSession(); + defaultSession = _repository.login(); + return _doImportInternal(parentSiteId, siteName, importerProperties, archiveSession, defaultSession); + } + finally + { + if (archiveSession != null) + { + archiveSession.logout(); + } + + if (defaultSession != null) + { + defaultSession.logout(); + } + } + } + + + /** + * Internal method where the import is actually done. + * @param parentSiteId + * @param siteName + * @param importerProperties + * @param importSession + * @param defaultSession + * @return The imported node + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + protected Node _doImportInternal(String parentSiteId, String siteName, Map importerProperties, Session importSession, Session defaultSession) throws RepositoryException, SAXException, IOException + { + ImportReferenceTracker referenceTracker = new ImportReferenceTracker(); + + // import workflow + ImportWorkflowHandler importWorkflowHandler = _importWorkflow(importSession, defaultSession, importerProperties.get(WORKFLOWS_PROPERTIES), referenceTracker); + + // import site node + ImportAmetysObjectHandler importSiteHandler = _importSiteNode(parentSiteId, siteName, importSession, defaultSession, importerProperties.get(SITE_NODE_PROPERTIES), importWorkflowHandler.getIdMapping(), referenceTracker); + Node siteNode = importSiteHandler.getImportedNode(); + + // adjust references + _adjustReferenceProperties(siteNode, referenceTracker); + + // workflows and site node are now imported, a save is possible. + importSession.save(); + defaultSession.save(); + + // import resources + _importResources(siteNode, importerProperties.get(RESOURCES_PROPERTIES)); + + // import binary properties + _importBinaryProperties(importSession, importerProperties.get(BINARY_PROPERTIES)); + + // adjust versionable nodes + _adjustVersionableNodes(siteNode, importSiteHandler.getExportDate()); + + // final save + importSession.save(); + + return siteNode; + } + + /** + * Import the workflow from the given input stream. + * @param importSession + * @param defaultSession + * @param properties + * @param referenceTracker + * @return ImportWorkflowHandler + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + protected ImportWorkflowHandler _importWorkflow(Session importSession, Session defaultSession, Properties properties, ImportReferenceTracker referenceTracker) throws RepositoryException, SAXException, IOException + { + InputStream inputStream = (InputStream) properties.get(WorkflowImporter.INPUT_STREAM_PROPERTY); + return _workflowImporter.doImport(importSession, defaultSession, inputStream, referenceTracker); + } + + /** + * Import the site node from a given input stream. + * @param parentSiteId + * @param siteName + * @param importSession + * @param defaultSession + * @param properties + * @param workflowIdMapping + * @param referenceTracker + * @return ImportAmetysObjectHandler + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + protected ImportAmetysObjectHandler _importSiteNode(String parentSiteId, String siteName, Session importSession, Session defaultSession, Properties properties, Map workflowIdMapping, ImportReferenceTracker referenceTracker) throws RepositoryException, SAXException, IOException + { + InputStream inputStream = (InputStream) properties.get(SiteNodeImporter.INPUT_STREAM_PROPERTY); + return _siteNodeImporter.doImport(parentSiteId, siteName, importSession, defaultSession, inputStream, workflowIdMapping, referenceTracker); + } + + /** + * Import the binary properties + * @param importSession + * @param properties + * @throws RepositoryException + */ + protected void _importBinaryProperties(Session importSession, Properties properties) throws RepositoryException + { + List importDataList = (List) properties.get(BinaryPropertiesImporter.IMPORT_DATA_LIST_PROPERTY); + _binaryPropertiesImporter.doImport(importSession, importDataList); + } + + /** + * Import the ametys resources + * @param siteNode + * @param properties + * @throws IOException + * @throws SAXException + * @throws RepositoryException + */ + protected void _importResources(Node siteNode, Properties properties) throws RepositoryException, SAXException, IOException + { + List nodeDataList = (List) properties.get(ResourcesImporter.IMPORT_RESOURCE_NODE_LIST_PROPERTY); + List binaryDataList = (List) properties.get(ResourcesImporter.IMPORT_BINARY_RESOURCE_LIST_PROPERTY); + _resourcesImporter.doImport(siteNode, nodeDataList, binaryDataList); + } + + /** + * Retrieve a session bound to the 'archives' workspace. Create and + * initialize the 'archives' workspace if necessary. + * + * @return + * @throws RepositoryException + */ + private Session _getArchiveSession() throws RepositoryException + { + try + { + return _repository.login(ArchiveConstants.ARCHIVE_WORKSPACE); + } + catch (NoSuchWorkspaceException e) + { + _repository.login().getWorkspace().createWorkspace(ArchiveConstants.ARCHIVE_WORKSPACE); + + Session archiveSession = _repository.login(ArchiveConstants.ARCHIVE_WORKSPACE); + archiveSession.getRootNode().addNode(AmetysObjectResolver.ROOT_REPO, AmetysObjectResolver.ROOT_TYPE); + return archiveSession; + } + } + + @Override + protected void _additionalAdjustementForVersionableNode(Node node, VersionHistory versionHistory) throws RepositoryException + { + Workspace importWorkspace = node.getSession().getWorkspace(); + boolean isArchiveWS = ArchiveConstants.ARCHIVE_WORKSPACE.equals(importWorkspace.getName()); + + // Add live label on validated version. + if (!isArchiveWS) + { + _addLiveLabelIfNecessary(node, versionHistory); + } + } + + /** + * Add the live label on the current version if necessary + * @param node + * @param versionHistory + * @throws RepositoryException + */ + protected void _addLiveLabelIfNecessary(Node node, VersionHistory versionHistory) throws RepositoryException + { + if (node.isNodeType(IOConstants.DEFAULT_CONTENT_NODETYPE) && node.hasProperty(IOConstants.WORKFLOW_ID_PROPERTY)) + { + List currentSteps = _workflow.getCurrentSteps(node.getProperty(IOConstants.WORKFLOW_ID_PROPERTY).getLong()); + Step currentStep = currentSteps.iterator().next(); + + if (currentStep instanceof AmetysStep) + { + Boolean validation = (Boolean) ((AmetysStep) currentStep).getProperty("validation"); + if (BooleanUtils.isTrue(validation)) + { + Iterator versionsIterator = IOUtils.getVersionSortedByCreatedDesc(versionHistory); + Version currentVersion = versionsIterator.next(); + versionHistory.addVersionLabel(currentVersion.getName(), WebConstants.LIVE_LABEL, true); + } + } + } + } +} Index: main/plugin-web/src/org/ametys/web/site/io/ImportSiteAction.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/ImportSiteAction.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/ImportSiteAction.java (revision 0) @@ -0,0 +1,423 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed 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. + */ +package org.ametys.web.site.io; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.acting.ServiceableAction; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Redirector; +import org.apache.cocoon.environment.Request; +import org.apache.cocoon.environment.SourceResolver; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOUtils; +import org.ametys.cms.io.importers.BinaryPropertiesImporter; +import org.ametys.cms.io.importers.ResourcesImporter; +import org.ametys.cms.io.importers.ResourcesImporter.BinaryResourceImportData; +import org.ametys.cms.io.importers.WorkflowImporter; +import org.ametys.web.live.RebuildLiveComponent; +import org.ametys.web.repository.content.jcr.DefaultWebContent; +import org.ametys.web.repository.site.Site; +import org.ametys.web.repository.site.SiteManager; +import org.ametys.web.site.io.importers.SiteImporter; +import org.ametys.web.site.io.importers.SiteNodeImporter; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * This action import a site from a file located in "java.io.tmpdir" + */ +public class ImportSiteAction extends ServiceableAction +{ + /** The site importer */ + protected SiteImporter _siteImporter; + + /** The site manager */ + protected SiteManager _siteManager; + + /** The rebuild live component */ + protected RebuildLiveComponent _rebuildLiveComponent; + + @Override + public void service(ServiceManager smanager) throws ServiceException + { + super.service(smanager); + _siteImporter = (SiteImporter) smanager.lookup(SiteImporter.ROLE); + _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); + _rebuildLiveComponent = (RebuildLiveComponent) smanager.lookup(RebuildLiveComponent.ROLE); + } + + @Override + public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception + { + Map result = new LinkedHashMap(); + Request request = ObjectModelHelper.getRequest(objectModel); + + String siteName = request.getParameter("siteName"); + String tmpFilePath = request.getParameter("tmpFilePath"); + boolean deleteFile = new Boolean(request.getParameter("deleteFile")); + String parentSiteId = request.getParameter("parentId"); + + // Site name must be unique. + if (_siteManager.hasSite(siteName)) + { + String msg = "Unable to import site. The site '" + siteName + "' already exists."; + getLogger().error(msg); + result.put("success", false); + result.put("error", msg); + return result; + } + + File tmpArchiveFile = null; + ZipFile siteZipFile = null; + boolean rebuildError = false; + try + { + tmpArchiveFile = new File(tmpFilePath); + siteZipFile = new ZipFile(tmpArchiveFile, "UTF-8", true); + + Site site = _importSiteFromZip(siteZipFile, parentSiteId, siteName); + + // Rebuild live of the site + try + { + _rebuildLiveComponent.rebuildLive(site); + } + catch (Exception e) + { + rebuildError = true; + throw e; + } + + result.put("success", true); + } + catch (Exception e) + { + String msg = ""; + if (rebuildError) + { + msg = "The site '" + siteName + "' has been successfully imported but the live rebuilt of the site has failed.\nTry rebuilding the live of a site again."; + } + else + { + msg = "Unable to import the site '" + siteName + "'"; + } + + getLogger().error(msg, e); + + msg += "\nError message : " + e; + result.put("success", false); + result.put("error", msg); + } + finally + { + if (siteZipFile != null) + { + siteZipFile.close(); + } + + if (deleteFile) + { + FileUtils.deleteQuietly(tmpArchiveFile); + } + } + + return result; + } + + /** + * Traverse the site archive and import data on the fly. + * @param siteZipFile The ZipFile of the archive + * @param parentSiteId The parent {@link Site} object. + * @param siteName Name of the site to be imported + * @return The imported site. + * @throws IOException + * @throws SAXException + * @throws RepositoryException + */ + private Site _importSiteFromZip(ZipFile siteZipFile, String parentSiteId, String siteName) throws IOException, SAXException, RepositoryException + { + ZipArchiveEntry entry = null; + + // Traverse the site archive to class entry by workspace (default or archives). + Set archivesEntries = new HashSet(); + Set defaultEntries = new HashSet(); + for (Enumeration entries = siteZipFile.getEntries(); entries.hasMoreElements();) + { + entry = entries.nextElement(); + if (!entry.isDirectory()) + { + String entryName = entry.getName(); + if (entryName.startsWith(IOConstants.ARCHIVES_DIR)) + { + archivesEntries.add(entry); + } + else + { + defaultEntries.add(entry); + } + } + } + + // Import site (default + archives workspace) + _importSite(siteZipFile, defaultEntries, parentSiteId, siteName); + _importArchives(siteZipFile, archivesEntries, parentSiteId, siteName); + + return _siteManager.getSite(siteName); + } + + private void _importSite(ZipFile siteZipFile, Set defaultEntries, String parentSiteId, String siteName) throws IOException, RepositoryException, SAXException + { + _importSite(siteZipFile, defaultEntries, parentSiteId, siteName, false); + } + + private void _importArchives(ZipFile siteZipFile, Set archivesEntries, String parentSiteId, String siteName) throws IOException, RepositoryException, SAXException + { + if (archivesEntries.isEmpty()) + { + return; + } + + _importSite(siteZipFile, archivesEntries, parentSiteId, siteName, true); + } + + private void _importSite(ZipFile siteZipFile, Set entries, String parentSiteId, String siteName, boolean isArchivesWS) throws IOException, RepositoryException, SAXException + { + String baseEntryName = isArchivesWS ? IOConstants.ARCHIVES_DIR : StringUtils.EMPTY; + + // Configure site import + Map siteImporterProperties = new HashMap(); + Properties props = new Properties(); + ZipArchiveEntry siteEntry = siteZipFile.getEntry(baseEntryName + IOSiteConstants.SITE_FILE_NAME); + props.put(SiteNodeImporter.INPUT_STREAM_PROPERTY, siteZipFile.getInputStream(siteEntry)); + siteImporterProperties.put(SiteImporter.SITE_NODE_PROPERTIES, props); + + // Configure workflow import + props = new Properties(); + ZipArchiveEntry workflowEntry = siteZipFile.getEntry(baseEntryName + IOConstants.WORKFLOWS_FILE_NAME); + props.put(WorkflowImporter.INPUT_STREAM_PROPERTY, siteZipFile.getInputStream(workflowEntry)); + siteImporterProperties.put(SiteImporter.WORKFLOWS_PROPERTIES, props); + + // Traverse the zip file class entries by type (data or resources) + Set dataEntries = new HashSet(); + Set resourcesEntries = new HashSet(); + for (ZipArchiveEntry entry : entries) + { + String entryName = entry.getName(); + if (entryName.startsWith(IOConstants.BINARY_DIR)) + { + dataEntries.add(entry); + } + else if (entryName.startsWith(IOConstants.EXPLORER_DIR) || entryName.startsWith(IOSiteConstants.ATTACHMENTS_DIR)) + { + resourcesEntries.add(entry); + } + } + + // Configure ametys resources import + props = _getResourcesProperties(siteZipFile, resourcesEntries, baseEntryName); + siteImporterProperties.put(SiteImporter.RESOURCES_PROPERTIES, props); + + // Configure binary properties import + props = _getBinaryDataProperties(siteZipFile, dataEntries); + siteImporterProperties.put(SiteImporter.BINARY_PROPERTIES, props); + + // Run the import + if (isArchivesWS) + { + _siteImporter.doImportArchives(parentSiteId, siteName, siteImporterProperties); + } + else + { + _siteImporter.doImport(parentSiteId, siteName, siteImporterProperties); + } + } + + /** + * Retrieves the Properties needed to configure the + * BinaryPropertyImporter used in the site import. + * + * @param siteZipFile + * @param dataEntries Entries representing a binary property to import. + * @return A Properties object to pass the to site importer. + * @throws IOException + * @see BinaryPropertiesImporter + */ + protected Properties _getBinaryDataProperties(ZipFile siteZipFile, Set dataEntries) throws IOException + { + List importDataList = new ArrayList(); + + // Iterate through all data entries, and populate the import data list. + for (ZipArchiveEntry binaryEntry : dataEntries) + { + String[] splittedEntryName = StringUtils.split(binaryEntry.getName(), '/'); + int len = splittedEntryName.length; + String identifier = splittedEntryName[len - 2]; + String propertyName = IOUtils.decodeZipEntryName(splittedEntryName[len - 1]); + + importDataList.add(new BinaryPropertiesImporter.ImportData(identifier, propertyName, siteZipFile.getInputStream(binaryEntry))); + } + + Properties props = new Properties(); + props.put(BinaryPropertiesImporter.IMPORT_DATA_LIST_PROPERTY, importDataList); + + return props; + } + + /** + * Retrieves the Properties needed to configure the + * BinaryPropertyImporter used in the site import. + * + * @param siteZipFile + * @param resourcesEntries Entries representing an ametys resource to import. + * @param baseEntryName Beginning of the name of the resourcesEntries corresponding to a context (e.g "" or "archives/"..) + * @return A Properties object to pass the to site importer. + * @throws IOException + * @see ResourcesImporter + */ + protected Properties _getResourcesProperties(ZipFile siteZipFile, Set resourcesEntries, String baseEntryName) throws IOException + { + // Map linking the start of the entry name to the start of the relative path. + final Map entryPathMapper = new HashMap(); + entryPathMapper.put(IOConstants.EXPLORER_DIR, StringUtils.EMPTY); + entryPathMapper.put(IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.CONTENTS_ATTACHMENTS_DIR, IOConstants.CONTENTS_NODE_NAME + '/'); + entryPathMapper.put(IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.PAGES_ATTACHMENTS_DIR, IOSiteConstants.SITEMAPS_NODE_NAME + '/'); + + // Map linking the start of the entry name to the resource node name + final Map resourceNodeNameMapper = new HashMap(); + resourceNodeNameMapper.put(IOConstants.EXPLORER_DIR, IOConstants.RESOURCES_NODE_NAME); + resourceNodeNameMapper.put(IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.CONTENTS_ATTACHMENTS_DIR, DefaultWebContent.ATTACHMENTS_NODE_NAME); + resourceNodeNameMapper.put(IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.PAGES_ATTACHMENTS_DIR, DefaultWebContent.ATTACHMENTS_NODE_NAME); + + // Populate the node data list to be imported and a multimap + // (mappedEntries) that will be used in the next for-loop. + final ListMultimap mappedEntries = ArrayListMultimap.create(); + List nodeDataList = new ArrayList(); + for (ZipArchiveEntry resourceEntry : resourcesEntries) + { + // Ignores directory + if (resourceEntry.isDirectory()) + { + continue; + } + + // Retrieve the relative path of the resource node given the entry name. + String entryName = StringUtils.removeStart(resourceEntry.getName(), baseEntryName); + String entryStart = null; + String relPath = null; + for (String start : entryPathMapper.keySet()) + { + if (entryName.startsWith(start)) + { + entryStart = start; + relPath = entryPathMapper.get(start) + StringUtils.removeStart(entryName, start); + } + } + + if (relPath == null || entryStart == null) + { + String msg = "Resource entry name is not valid."; + getLogger().error(msg); + throw new IOException(msg); + } + + // Import resource.sv files. + if (_isResourceSvFilePath(relPath)) + { + relPath = StringUtils.removeEnd(relPath, IOConstants.RESOURCES_FILE_NAME); + relPath = StringUtils.removeEnd(relPath, "/"); + nodeDataList.add(new ResourcesImporter.ResourceNodeImportData(relPath, siteZipFile.getInputStream(resourceEntry))); + } + // Populate the mappedEntries multimap. + else + { + // At this stage, path should contains the special "_resources" folder. + relPath = relPath.startsWith(IOConstants.RESOURCES_DIR) ? StringUtils.EMPTY : StringUtils.substringBefore(relPath, '/' + IOConstants.RESOURCES_DIR) + '/'; + relPath += resourceNodeNameMapper.get(entryStart); + mappedEntries.put(relPath, resourceEntry); + } + } + + // Populate the binary resource data list to be imported + List binaryDataList = new ArrayList(); + for (String resourceCollectionPath : mappedEntries.keySet()) + { + for (ZipArchiveEntry entry : mappedEntries.get(resourceCollectionPath)) + { + String entryName = entry.getName(); + String resourcePath = null; + if (entryName.startsWith(IOConstants.RESOURCES_DIR)) + { + resourcePath = StringUtils.removeStart(entryName, IOConstants.RESOURCES_DIR); + } + else + { + resourcePath = StringUtils.substringAfter(entryName, '/' + IOConstants.RESOURCES_DIR); + } + resourcePath = IOUtils.decodeZipEntryName(resourcePath); + + binaryDataList.add(new BinaryResourceImportData(resourceCollectionPath + '/' + resourcePath, siteZipFile.getInputStream(entry))); + } + } + + Properties props = new Properties(); + props.put(ResourcesImporter.IMPORT_RESOURCE_NODE_LIST_PROPERTY, nodeDataList); + props.put(ResourcesImporter.IMPORT_BINARY_RESOURCE_LIST_PROPERTY, binaryDataList); + + return props; + } + + /** + * Indicates if a relative path denotes a path to a XML "resource" system + * view. The path must end with {@link IOConstants#RESOURCES_FILE_NAME} + * and must not contain a folder equals to + * {@link IOConstants#RESOURCES_DIR} + * @param relPath The relative path to be tested + * @return True if relPath match the conditions above. + */ + protected boolean _isResourceSvFilePath(String relPath) + { + if (relPath.endsWith(IOConstants.RESOURCES_FILE_NAME)) + { + String mustNotContain = StringUtils.removeEnd(IOConstants.RESOURCES_DIR, "/"); + return !ArrayUtils.contains(StringUtils.split(relPath, '/'), mustNotContain); + } + return false; + } +} Index: main/plugin-web/src/org/ametys/web/site/io/UploadSiteAction.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/UploadSiteAction.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/UploadSiteAction.java (revision 0) @@ -0,0 +1,127 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed 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. + */ +package org.ametys.web.site.io; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.acting.ServiceableAction; +import org.apache.cocoon.environment.ObjectModelHelper; +import org.apache.cocoon.environment.Redirector; +import org.apache.cocoon.environment.Request; +import org.apache.cocoon.environment.SourceResolver; +import org.apache.cocoon.servlet.multipart.Part; +import org.apache.cocoon.servlet.multipart.PartOnDisk; +import org.apache.cocoon.servlet.multipart.RejectedPart; +import org.apache.commons.io.FileUtils; + +import org.ametys.runtime.cocoon.JSonReader; +import org.ametys.web.repository.site.SiteManager; + +/** + * This action receives a form with the "importfile" file. + * This file contains a serialized site to be imported. + */ +public class UploadSiteAction extends ServiceableAction +{ + /** The site manager */ + protected SiteManager _siteManager; + + @Override + public void service(ServiceManager smanager) throws ServiceException + { + super.service(smanager); + _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); + } + + @Override + public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception + { + Request request = ObjectModelHelper.getRequest(objectModel); + Map result = new LinkedHashMap(); + + try + { + // Checking file + Part part = (Part) request.get("importfile"); + if (part instanceof RejectedPart || part == null) + { + result.put("success", false); + result.put("error", "rejected"); + } + else + { + // Getting file + PartOnDisk uploadedFilePart = (PartOnDisk) part; + File uploadedFile = uploadedFilePart.getFile(); + + String filename = (String) uploadedFilePart.getHeaders().get("filename"); + String siteName = filename.substring(0, filename.length() - 4); + + // Site name must be unique. + if (!_siteManager.hasSite(siteName)) + { + // Copy the file into the default tmp dir. + File tmpFile = _copyToTmpDir(uploadedFile, siteName); + + result.put("tmpFilePath", tmpFile.getPath()); + result.put("siteName", siteName); + result.put("success", true); + } + else + { + String msg = "Unable to upload site. The site '" + siteName + "' already exists."; + getLogger().error(msg); + result.put("error", "site-existing"); + result.put("success", false); + } + } + } + catch (Exception e) + { + getLogger().error("Unable to upload the site", e); + result.put("success", false); + + Map ex = new HashMap(); + ex.put("message", e.getMessage()); + + result.put("error", ex); + } + + request.setAttribute(JSonReader.MAP_TO_READ, result); + return EMPTY_MAP; + } + + /** + * Copy the uploaded file in a temporary directory. + * @param uploadedFile the uploaded file + * @param siteName + * @return the copied file instance + * @throws IOException + */ + protected File _copyToTmpDir(File uploadedFile, String siteName) throws IOException + { + File destFile = File.createTempFile(siteName, ".zip"); + FileUtils.copyFile(uploadedFile, destFile); + return destFile; + } +} Index: main/plugin-web/src/org/ametys/web/site/io/handlers/ExportSiteHandler.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/handlers/ExportSiteHandler.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/handlers/ExportSiteHandler.java (revision 0) @@ -0,0 +1,97 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed 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. + */ +package org.ametys.web.site.io.handlers; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.lang.StringUtils; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.handlers.ExportAmetysObjectHandler; +import org.ametys.web.repository.content.jcr.DefaultWebContent; +import org.ametys.web.repository.site.SiteManager; + +/** + * "Proxy" handler used to export an ametys object. + */ +public class ExportSiteHandler extends ExportAmetysObjectHandler +{ + /** + * Ctor inheritance + * @param contentHandler The {@link ContentHandler} to be wrapped. + * @param parentNode The node from which the import/export is relative + */ + public ExportSiteHandler(final ContentHandler contentHandler, Node parentNode) + { + super(contentHandler, parentNode); + + _nodesToSkip.add(IOConstants.RESOURCES_NODE_NAME); + _nodesToSkip.add(SiteManager.ROOT_SITES); + _nodesToSkip.add(DefaultWebContent.ATTACHMENTS_NODE_NAME); + } + + @Override + protected boolean _onNodeToSkip(String name) throws SAXException + { + // Attachments nodes are not exported if they contain an attachment. + // In this case, the attachment must be exported as a resource later. + String path = (String) _nodeStack.peek().get(PATH_PROPERTY_NAME); + if (DefaultWebContent.ATTACHMENTS_NODE_NAME.equals(name)) + { + try + { + Node attachments = _parentNode.getParent().getNode(path); + if (attachments.hasNodes()) + { + _resources.add(path); + } + else + { + return false; + } + } + catch (RepositoryException e) + { + String msg = "Error during the export of node of type '" + DefaultWebContent.ATTACHMENTS_NODE_NAME + "' node."; + _logger.error(msg, e); + throw new SAXException(msg, e); + } + } + else if (IOConstants.RESOURCES_NODE_NAME.equals(name)) + { + _resources.add(path); + } + + return true; + } + + @Override + protected boolean _isExternal(Item referencedItem) throws RepositoryException + { + String asbRefItemPath = referencedItem.getPath(); + + String siteNodePath = _parentNode.getPath(); + String relRefItemPath = StringUtils.removeStart(asbRefItemPath, siteNodePath + '/'); + + // To be considered as external, referenced item should not belong to the site. + // It can be somewhere outside the site, or somewhere in a child site of the site + return !asbRefItemPath.startsWith(siteNodePath) || relRefItemPath.contains(SiteManager.ROOT_SITES + '/'); + } +} Index: main/plugin-web/src/org/ametys/web/site/io/IOSiteConstants.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/IOSiteConstants.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/IOSiteConstants.java (revision 0) @@ -0,0 +1,29 @@ +package org.ametys.web.site.io; + +import org.ametys.cms.io.IOConstants; +import org.ametys.plugins.repository.RepositoryConstants; + +/** + * Collection of constants used in the import/export of a site + */ +public final class IOSiteConstants +{ + /** Archive site filename */ + public static final String SITE_FILE_NAME = "site" + IOConstants.SYSTEM_VIEW_EXT; + /** Constant for contents node name. */ + public static final String SITEMAPS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sitemaps"; + + /** Archive attachments data folder */ + public static final String ATTACHMENTS_DIR = "attachments/"; + /** Archive content data folder */ + public static final String CONTENTS_ATTACHMENTS_DIR = "contents/"; + /** Archive page data folder */ + public static final String PAGES_ATTACHMENTS_DIR = "pages/"; + + /** + * Private constructor to prevent instantiation of this class. + */ + private IOSiteConstants() + { + } +} Index: main/plugin-web/src/org/ametys/web/site/io/ExportSiteArchiver.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/ExportSiteArchiver.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/ExportSiteArchiver.java (revision 0) @@ -0,0 +1,592 @@ +/* + * Copyright 2012 Anyware Services + * + * Licensed 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. + */ +package org.ametys.web.site.io; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.cocoon.ProcessingException; +import org.apache.cocoon.reading.ServiceableReader; +import org.apache.cocoon.xml.XMLUtils; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream.UnicodeExtraFieldPolicy; +import org.apache.commons.lang.StringUtils; +import org.apache.xml.serializer.OutputPropertiesFactory; +import org.xml.sax.SAXException; + +import org.ametys.cms.content.archive.ArchiveConstants; +import org.ametys.cms.io.IOConstants; +import org.ametys.cms.io.IOUtils; +import org.ametys.cms.io.exporters.AmetysExporter; +import org.ametys.cms.io.exporters.DefaultJcrExportListener; +import org.ametys.cms.io.exporters.LogExporter; +import org.ametys.cms.io.exporters.ResourcesExporter; +import org.ametys.cms.repository.Content; +import org.ametys.plugins.repository.AmetysObjectIterable; +import org.ametys.plugins.repository.AmetysObjectResolver; +import org.ametys.plugins.repository.UnknownAmetysObjectException; +import org.ametys.plugins.repository.provider.AbstractRepository; +import org.ametys.runtime.util.IgnoreRootHandler; +import org.ametys.web.repository.content.jcr.DefaultWebContent; +import org.ametys.web.repository.site.Site; +import org.ametys.web.site.io.exporters.SiteExporter; + +/** + * Generate a ZIP Archive for a given site. + */ +public class ExportSiteArchiver extends ServiceableReader +{ + /** The final transformer handler */ + protected TransformerHandler _transformerHandler; + + /** The Zip stream where entries will be written */ + protected ZipArchiveOutputStream _zipOutput; + + /** The Ametys object resolver */ + protected AmetysObjectResolver _resolver; + + /** JCR Repository */ + protected Repository _repository; + + @Override + public void service(ServiceManager sManager) throws ServiceException + { + super.service(sManager); + _resolver = (AmetysObjectResolver) sManager.lookup(AmetysObjectResolver.ROLE); + _repository = (Repository) sManager.lookup(AbstractRepository.ROLE); + } + + @Override + public String getMimeType() + { + return "application/zip"; + } + + @Override + public void generate() throws IOException, SAXException, ProcessingException + { + String siteId = parameters.getParameter("id", null); + + try + { + // Create a ZIP OutputStream that writes into the reader OutputStream + _zipOutput = new ZipArchiveOutputStream(out); + _zipOutput.setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy.ALWAYS); + _zipOutput.setEncoding("UTF-8"); + + // Parameterize the handlers + _initializeTransformerHandler(); + + // Export the site into the ZIP Archive. + _exportSite(siteId); + _exportArchives(siteId); + } + finally + { + if (_zipOutput != null) + { + _zipOutput.finish(); + _zipOutput.close(); + } + } + } + + private void _exportSite(String siteId) throws IOException + { + Site site = _resolver.resolveById(siteId); + _exportSite(site); + } + + private void _exportSite(Site site) throws IOException + { + _exportSite(site, StringUtils.EMPTY); + } + + private void _exportSite(Site site, final String baseEntryName) throws IOException + { + if (StringUtils.isNotEmpty(baseEntryName)) + { + // Create base folder entry. + ZipArchiveEntry baseDir = new ZipArchiveEntry(baseEntryName); + _zipOutput.putArchiveEntry(baseDir); + _zipOutput.closeArchiveEntry(); + } + + + SiteExporter siteExporter = new ArchiveSiteExporter(baseEntryName, site); + + // Configure export + // site node + Properties props = new Properties(); + props.put(AmetysExporter.CONTENT_HANDLER_PROPERTY, _transformerHandler); + siteExporter.setProperties(SiteExporter.SITE_NODE_PROPERTIES, props); + + // workflows + props = new Properties(); + props.put(AmetysExporter.CONTENT_HANDLER_PROPERTY, new IgnoreRootHandler(_transformerHandler)); + siteExporter.setProperties(SiteExporter.WORKFLOWS_PROPERTIES, props); + + // ametys resources + props = new Properties(); + props.put(AmetysExporter.CONTENT_HANDLER_PROPERTY, _transformerHandler); + props.put(AmetysExporter.OUTPUT_STREAM_PROPERTY, _zipOutput); + props.put(AmetysExporter.LISTENER_PROPERTY, new ResourcesExporterListener(baseEntryName, site)); + siteExporter.setProperties(SiteExporter.RESOURCES_PROPERTIES, props); + + // binary properties + props = new Properties(); + props.put(AmetysExporter.OUTPUT_STREAM_PROPERTY, _zipOutput); + props.put(AmetysExporter.LISTENER_PROPERTY, new BinaryPropertyExportListener(baseEntryName)); + siteExporter.setProperties(SiteExporter.BINARY_PROPERTIES, props); + + // logs + props = new Properties(); + props.put(AmetysExporter.OUTPUT_STREAM_PROPERTY, _zipOutput); + siteExporter.setProperties(SiteExporter.LOGS_PROPERTIES, props); + + // Run the export. + siteExporter.export(); + } + + private void _exportArchives(String siteId) throws IOException, ProcessingException + { + Session session = null; + try + { + session = _repository.login(ArchiveConstants.ARCHIVE_WORKSPACE); + Site site = null; + try + { + site = _resolver.resolveById(siteId, session); + AmetysObjectIterable archivedContents = site.getContents(); + if (!archivedContents.hasNext()) + { + // Nothing to export + return; + } + } + catch (UnknownAmetysObjectException e) + { + // Nothing to export + return; + } + + _exportSite(site, IOConstants.ARCHIVES_DIR); + } + catch (NoSuchWorkspaceException e) + { + // ignores + } + catch (RepositoryException e) + { + getLogger().error("Unable to export the archives of site with id '" + siteId + "'", e); + throw new ProcessingException(e); + } + finally + { + if (session != null) + { + session.logout(); + } + } + } + + private void _initializeTransformerHandler() throws ProcessingException + { + try + { + _transformerHandler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); + } + catch (Exception e) + { + getLogger().error("Unable to export the site", e); + throw new ProcessingException(e); + } + + // create the format of result + final Properties properties = new Properties(); + properties.put(OutputKeys.METHOD, "xml"); + properties.put(OutputKeys.INDENT, "yes"); + properties.put(OutputKeys.ENCODING, "UTF-8"); + properties.put(OutputKeys.OMIT_XML_DECLARATION, "no"); + properties.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); + + _transformerHandler.getTransformer().setOutputProperties(properties); + + // create the result where to write + StreamResult sResult = new StreamResult(_zipOutput); + _transformerHandler.setResult(sResult); + } + + @Override + public void recycle() + { + this._transformerHandler = null; + _zipOutput = null; + } + + /** + * ArchiveSiteExporter + */ + protected class ArchiveSiteExporter extends SiteExporter + { + /** Prefix of the name of the created entries */ + protected final String _baseEntryName; + + /** + * Ctor + * @param baseEntryName + * @param site + */ + public ArchiveSiteExporter(String baseEntryName, Site site) + { + super(site); + _baseEntryName = baseEntryName; + } + + @Override + public void onSiteNodeExportStart() throws IOException + { + String entryName = _baseEntryName + IOSiteConstants.SITE_FILE_NAME; + _zipOutput.putArchiveEntry(new ZipArchiveEntry(entryName)); + } + + @Override + public void onSiteNodeExportEnd() throws IOException + { + _zipOutput.closeArchiveEntry(); + } + + @Override + public void onWorkflowsExportStart(Set workflowRefs) throws Exception + { + if (!workflowRefs.isEmpty()) + { + String entryName = _baseEntryName + IOConstants.WORKFLOWS_FILE_NAME; + _zipOutput.putArchiveEntry(new ZipArchiveEntry(entryName)); + + // Root element creation. + _transformerHandler.startDocument(); + XMLUtils.startElement(_transformerHandler, IOConstants.WORKFLOW_FILE_ROOT_NODE); + } + } + + @Override + public void onWorkflowsExportEnd(Set workflowRefs) throws Exception + { + if (!workflowRefs.isEmpty()) + { + // End of the root element + XMLUtils.endElement(_transformerHandler, IOConstants.WORKFLOW_FILE_ROOT_NODE); + _transformerHandler.endDocument(); + + _zipOutput.closeArchiveEntry(); + } + } + + @Override + public void onBinaryPropertiesExportStart(Map> binaryMap) throws Exception + { + if (!binaryMap.isEmpty()) + { + String entryName = _baseEntryName + IOConstants.BINARY_DIR; + _zipOutput.putArchiveEntry(new ZipArchiveEntry(entryName)); + _zipOutput.closeArchiveEntry(); + } + } + + @Override + public void onResourcesExportStart(Set paths) throws Exception + { + if (paths.isEmpty()) + { + return; + } + + // Classify resource by context (explorer / contents / pages) + Set explorerResources = new HashSet(); + Set contentResources = new HashSet(); + Set pageResources = new HashSet(); + final String basePath = _site.getName() + '/'; + + for (String path : paths) + { + if (path.startsWith(basePath + IOConstants.RESOURCES_NODE_NAME)) + { + explorerResources.add(path); + } + else if (path.startsWith(basePath + IOConstants.CONTENTS_NODE_NAME)) + { + contentResources.add(path); + } + else if (path.startsWith(basePath + IOSiteConstants.SITEMAPS_NODE_NAME)) + { + pageResources.add(path); + } + else + { + String msg = "Resource path do not start as expected."; + _logger.error(msg); + throw new RepositoryException(msg); + } + } + + if (explorerResources.size() >= 2) + { + String msg = "There should be at most one '" + IOConstants.RESOURCES_NODE_NAME + "' node to export"; + _logger.error(msg); + throw new RepositoryException(msg); + } + + if (!explorerResources.isEmpty()) + { + ZipArchiveEntry explorerEntryDir = new ZipArchiveEntry(_baseEntryName + IOConstants.EXPLORER_DIR); + _zipOutput.putArchiveEntry(explorerEntryDir); + _zipOutput.closeArchiveEntry(); + } + + if (!(contentResources.isEmpty() && pageResources.isEmpty())) + { + ZipArchiveEntry attEntryDir = new ZipArchiveEntry(_baseEntryName + IOSiteConstants.ATTACHMENTS_DIR); + _zipOutput.putArchiveEntry(attEntryDir); + _zipOutput.closeArchiveEntry(); + + if (!contentResources.isEmpty()) + { + ZipArchiveEntry contentsEntryDir = new ZipArchiveEntry(attEntryDir.getName() + IOSiteConstants.CONTENTS_ATTACHMENTS_DIR); + _zipOutput.putArchiveEntry(contentsEntryDir); + _zipOutput.closeArchiveEntry(); + } + + if (!pageResources.isEmpty()) + { + ZipArchiveEntry pagesEntryDir = new ZipArchiveEntry(attEntryDir.getName() + IOSiteConstants.PAGES_ATTACHMENTS_DIR); + _zipOutput.putArchiveEntry(pagesEntryDir); + _zipOutput.closeArchiveEntry(); + } + } + } + + @Override + public void onLogExportStart(Map> externalReferences) throws Exception + { + if (!externalReferences.isEmpty()) + { + ZipArchiveEntry entryDir = new ZipArchiveEntry(_baseEntryName + LogExporter.LOG_FILE_NAME); + _zipOutput.putArchiveEntry(entryDir); + } + } + + @Override + public void onLogExportEnd(Map> externalReferences) throws Exception + { + if (!externalReferences.isEmpty()) + { + _zipOutput.closeArchiveEntry(); + } + } + } + + /** + * BinaryPropertyExportListener + */ + protected class BinaryPropertyExportListener extends DefaultJcrExportListener + { + /** Prefix of the name of the created entries */ + protected final String _baseEntryName; + + /** + * Ctor + * @param baseEntryName + */ + public BinaryPropertyExportListener(String baseEntryName) + { + _baseEntryName = baseEntryName; + } + + @Override + public void onBeforeBinaryPropertyExport(Property property) throws RepositoryException, IOException + { + String entryName = _getEntryName(property); + _zipOutput.putArchiveEntry(new ZipArchiveEntry(entryName)); + } + + @Override + public void onAfterBinaryPropertyExport(Property property) throws IOException + { + _zipOutput.closeArchiveEntry(); + } + + /** + * Get the entry name in the archive for a given property. + * @param property + * @return The name of the entry. + * @throws RepositoryException + */ + protected String _getEntryName(Property property) throws RepositoryException + { + final String name = property.getName(); + final String parentIdentifier = property.getParent().getIdentifier(); + final String hashPath = StringUtils.join(IOUtils.getHashedName(parentIdentifier), '/') + '/'; + + String entryPath = IOConstants.BINARY_DIR + hashPath + parentIdentifier + '/'; + entryPath += IOUtils.encodeZipEntryName(name); + + return _baseEntryName + entryPath; + } + } + + /** + * ResourcesExporterListener + */ + protected class ResourcesExporterListener extends DefaultJcrExportListener + { + /** Map used to construct the beginning of the entry name. */ + protected final Map _addStartMapper = new HashMap(); + + /** Map used to exclude the start of the path of a resource in the entry name. */ + protected final Map _excludeStartMapper = new HashMap(); + + /** Map used to exclude the end of the path of a resource in the entry name. */ + protected final Map _excludeEndMapper = new HashMap(); + + /** Prefix of the name of the created entries */ + protected final String _baseEntryName; + + /** Base exported node */ + protected final Node _exportedNode; + + /** Resource node currently exported */ + protected Node _currentNode; + + /** Entry path of the last export resource node */ + protected String _currentResourceEntryPath; + + /** + * Ctor + * @param baseEntryName + * @param site + */ + public ResourcesExporterListener(String baseEntryName, Site site) + { + _baseEntryName = baseEntryName; + _exportedNode = site.getNode(); + + _addStartMapper.put(IOConstants.RESOURCES_NODE_NAME, _baseEntryName + IOConstants.EXPLORER_DIR); + _addStartMapper.put(IOConstants.CONTENTS_NODE_NAME, _baseEntryName + IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.CONTENTS_ATTACHMENTS_DIR); + _addStartMapper.put(IOSiteConstants.SITEMAPS_NODE_NAME, _baseEntryName + IOSiteConstants.ATTACHMENTS_DIR + IOSiteConstants.PAGES_ATTACHMENTS_DIR); + + _excludeStartMapper.put(IOConstants.RESOURCES_NODE_NAME, IOConstants.RESOURCES_NODE_NAME); + _excludeStartMapper.put(IOConstants.CONTENTS_NODE_NAME, IOConstants.CONTENTS_NODE_NAME); + _excludeStartMapper.put(IOSiteConstants.SITEMAPS_NODE_NAME, IOSiteConstants.SITEMAPS_NODE_NAME); + + _excludeEndMapper.put(IOConstants.RESOURCES_NODE_NAME, StringUtils.EMPTY); + _excludeEndMapper.put(IOConstants.CONTENTS_NODE_NAME, DefaultWebContent.ATTACHMENTS_NODE_NAME); + _excludeEndMapper.put(IOSiteConstants.SITEMAPS_NODE_NAME, DefaultWebContent.ATTACHMENTS_NODE_NAME); + } + + @Override + public void onBeforeNodeExport(Node node) throws Exception + { + _currentNode = node; + + // Evaluate the path of the resource in the archive. + _currentResourceEntryPath = _getEntryPath(node); + + // Export 'resource' system view + ZipArchiveEntry resFile = new ZipArchiveEntry(_currentResourceEntryPath + IOConstants.RESOURCES_FILE_NAME); + _zipOutput.putArchiveEntry(resFile); + } + + @Override + public void onAfterNodeExport(Node node) throws Exception + { + _zipOutput.closeArchiveEntry(); + } + + + /** + * Find the name of the entry for the given resource node + * @param resourceNode the resource node + * @return The entry name. + * @throws RepositoryException + */ + protected String _getEntryPath(Node resourceNode) throws RepositoryException + { + final String sitePath = _exportedNode.getPath(); + String relPath = StringUtils.removeStart(resourceNode.getPath(), sitePath + '/'); + + String key = StringUtils.substringBefore(relPath, "/"); + String excludeStart = _excludeStartMapper.get(key); + String excludeEnd = _excludeEndMapper.get(key); + String addStart = _addStartMapper.get(key); + + String entryPath = StringUtils.removeEnd(StringUtils.removeStart(relPath, excludeStart), excludeEnd); + entryPath = addStart + StringUtils.removeStart(entryPath, "/"); + + return entryPath.endsWith("/") ? entryPath : entryPath + '/'; + } + + @Override + public void onBeforeBinaryPropertyExport(Property property) throws Exception + { + String entryName = _getEntryPath(property); + ZipArchiveEntry resFile = new ZipArchiveEntry(entryName); + _zipOutput.putArchiveEntry(resFile); + } + + @Override + public void onAfterBinaryPropertyExport(Property property) throws Exception + { + _zipOutput.closeArchiveEntry(); + } + + /** + * Construct the name of the entry for the given property + * @param property + * @return The entry name. + * @throws RepositoryException + */ + protected String _getEntryPath(Property property) throws RepositoryException + { + final String currentNodePath = _currentNode.getPath(); + String relPath = StringUtils.removeStart(property.getPath(), currentNodePath + '/'); + relPath = StringUtils.removeEnd(relPath, '/' + ResourcesExporter.JCR_DATA_PATH); + + return _currentResourceEntryPath + IOConstants.RESOURCES_DIR + IOUtils.encodeZipEntryName(relPath); + } + } +} Index: main/plugin-web/src/org/ametys/web/site/io/exporters/SiteNodeExporter.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/exporters/SiteNodeExporter.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/exporters/SiteNodeExporter.java (revision 0) @@ -0,0 +1,97 @@ +package org.ametys.web.site.io.exporters; + +import java.util.Collection; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.apache.avalon.framework.logger.Logger; +import org.xml.sax.ContentHandler; + +import org.ametys.cms.io.exporters.JcrExporter; +import org.ametys.cms.io.exporters.WorkflowExporter; +import org.ametys.runtime.util.LoggerFactory; +import org.ametys.web.repository.site.Site; +import org.ametys.web.site.io.handlers.ExportSiteHandler; + +/** + * SiteNodeExporter is used to export the node of a site. It has some getter + * methods that may be used by other exporters (e.g. {@link WorkflowExporter}). + */ +public class SiteNodeExporter extends JcrExporter +{ + /** logger */ + protected final Logger _logger = LoggerFactory.getLoggerFor(SiteNodeExporter.class); + + /** The site to export */ + protected final Site _site; + /** The export handler */ + protected final ExportSiteHandler _exportSiteHandler; + + /** + * Ctor + * @param site + * @param contentHandler + */ + public SiteNodeExporter(Site site, ContentHandler contentHandler) + { + _site = site; + _exportSiteHandler = new ExportSiteHandler(contentHandler, site.getNode()); + } + + /** + * Export method. This is where the job is actually done. + * @param skipBinary + * @throws Exception + */ + public void export(boolean skipBinary) throws Exception + { + try + { + exportNode(_site.getNode(), _exportSiteHandler, skipBinary, true); + } + catch (Exception e) + { + // Log and re-throw + final String name = _site.getName(); + _logger.error("Unable to export the site node of site '" + name + "'", e); + throw e; + } + } + + /** + * Retrieves the workflow identifiers. + * @return the workflow identifiers + */ + public Set getWorkflowRefs() + { + return _exportSiteHandler.getWorkflowRefs(); + } + + /** + * Retrieves the binary properties. + * @return the binaryProperties + */ + public Map> getBinaryProperties() + { + return _exportSiteHandler.getBinaryProperties(); + } + + /** + * Retrieves the resources. + * @return the resources + */ + public Set getResources() + { + return _exportSiteHandler.getResources(); + } + + /** + * Retrieves the external references + * @return the externalReferences + */ + public Map> getExternalReferences() + { + return _exportSiteHandler.getExternalReferences(); + } +} Index: main/plugin-web/src/org/ametys/web/site/io/exporters/SiteExporter.java =================================================================== --- main/plugin-web/src/org/ametys/web/site/io/exporters/SiteExporter.java (revision 0) +++ main/plugin-web/src/org/ametys/web/site/io/exporters/SiteExporter.java (revision 0) @@ -0,0 +1,324 @@ +package org.ametys.web.site.io.exporters; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.xml.sax.ContentHandler; + +import org.ametys.cms.io.exporters.AmetysExporter; +import org.ametys.cms.io.exporters.BinaryPropertiesExporter; +import org.ametys.cms.io.exporters.JcrExportListener; +import org.ametys.cms.io.exporters.LogExporter; +import org.ametys.cms.io.exporters.ResourcesExporter; +import org.ametys.cms.io.exporters.WorkflowExporter; +import org.ametys.web.repository.site.Site; + +/** + * Configurable Site Exporter. Exports its content into + * ContentHandler and/or OutputStream + */ +public class SiteExporter extends AmetysExporter +{ + // Key used to configure the exporters + /** configuration key of the site node exporter */ + public static final String SITE_NODE_PROPERTIES = "siteNode"; + /** configuration key of the workflow exporter */ + public static final String WORKFLOWS_PROPERTIES = "workflows"; + /** configuration key of the resource exporter */ + public static final String RESOURCES_PROPERTIES = "resources"; + /** configuration key of the binary properties exporter */ + public static final String BINARY_PROPERTIES = "binary"; + /** configuration key of the logs exporter */ + public static final String LOGS_PROPERTIES = "logs"; + + /** Site to export */ + protected final Site _site; + + /** + * Ctor + * @param site + */ + public SiteExporter(Site site) + { + _site = site; + } + + @Override + protected void _exportInternal() throws IOException + { + try + { + final Session session = _site.getNode().getSession(); + + // Export the site node + onSiteNodeExportStart(); + SiteNodeExporter siteNodeExporter = _exportSiteNode(); + onSiteNodeExportEnd(); + + // Export workflow nodes + onWorkflowsExportStart(siteNodeExporter.getWorkflowRefs()); + _exportWorkflows(siteNodeExporter); + onWorkflowsExportEnd(siteNodeExporter.getWorkflowRefs()); + + // Export resources from the explorer. + onResourcesExportStart(siteNodeExporter.getResources()); + _exportResources(siteNodeExporter); + onResourcesExportEnd(siteNodeExporter.getResources()); + + // Export the binary properties + onBinaryPropertiesExportStart(siteNodeExporter.getBinaryProperties()); + _exportBinaryProperties(siteNodeExporter); + onBinaryPropertiesExportEnd(siteNodeExporter.getBinaryProperties()); + + // Export log file + onLogExportStart(siteNodeExporter.getExternalReferences()); + _exportLog(siteNodeExporter); + onLogExportEnd(siteNodeExporter.getExternalReferences()); + + // Do not save changes. + session.refresh(false); + } + catch (Exception e) + { + _logger.error("Unable to export the site '" + _site.getName() + "'", e); + throw new IOException(e); + } + } + + /** + * Export the node corresponding to the Site object. + * @return The exporter used to export the site node, which is a {@link SiteNodeExporter} + * @throws Exception + */ + protected SiteNodeExporter _exportSiteNode() throws Exception + { + Properties props = _getSiteNodeProperties(); + SiteNodeExporter exporter = new SiteNodeExporter(_site, (ContentHandler) props.get(CONTENT_HANDLER_PROPERTY)); + + JcrExportListener listener = (JcrExportListener) props.get(LISTENER_PROPERTY); + exporter.setListener(listener); + + exporter.export(true); // skip binary, they will be exported later. + + return exporter; + } + + /** + * Export the workflow nodes which references have been collected during the + * export of the site node. + * + * @param siteNodeExporter The exporter used to export the site node. + * @throws Exception + */ + protected void _exportWorkflows(SiteNodeExporter siteNodeExporter) throws Exception + { + Properties props = _getWorkflowProperties(); + + final Session session = _site.getNode().getSession(); + WorkflowExporter exporter = new WorkflowExporter(siteNodeExporter.getWorkflowRefs(), session, (ContentHandler) props.get(CONTENT_HANDLER_PROPERTY)); + JcrExportListener listener = (JcrExportListener) props.get(LISTENER_PROPERTY); + exporter.setListener(listener); + + exporter.export(); + } + + /** + * Export the ametys resources nodes that have been collected during the + * export of the site node. + * + * @param siteNodeExporter The exporter used to export the site node. + * @throws Exception + */ + protected void _exportResources(SiteNodeExporter siteNodeExporter) throws Exception + { + Properties props = _getResourcesProperties(); + ResourcesExporter exporter = new ResourcesExporter(_site.getNode().getParent(), siteNodeExporter.getResources(), (ContentHandler) props.get(CONTENT_HANDLER_PROPERTY), (OutputStream) props.get(OUTPUT_STREAM_PROPERTY)); + JcrExportListener listener = (JcrExportListener) props.get(LISTENER_PROPERTY); + exporter.setListener(listener); + + exporter.export(); + } + + /** + * Export the binary properties (jcr) that have been collected during the + * export of the site node. + * + * @param siteNodeExporter The exporter used to export the site node. + * @throws Exception + */ + protected void _exportBinaryProperties(SiteNodeExporter siteNodeExporter) throws Exception + { + Properties props = _getBinaryPropertiesProperties(); + + BinaryPropertiesExporter exporter = new BinaryPropertiesExporter(_site.getNode().getParent(), siteNodeExporter.getBinaryProperties(), (OutputStream) props.get(OUTPUT_STREAM_PROPERTY)); + JcrExportListener listener = (JcrExportListener) props.get(LISTENER_PROPERTY); + exporter.setListener(listener); + + exporter.export(); + } + + /** + * Export some logs + * @param siteNodeExporter + * @throws RepositoryException + * @see LogExporter + */ + protected void _exportLog(SiteNodeExporter siteNodeExporter) throws RepositoryException + { + Properties props = _getLogProperties(); + + final Session session = _site.getNode().getSession(); + LogExporter exporter = new LogExporter(siteNodeExporter.getExternalReferences(), session, (OutputStream) props.get(OUTPUT_STREAM_PROPERTY)); + + exporter.export(); + } + + /** + * Retrieves the properties of the site node exporter. + * @return these configured properties + */ + protected Properties _getSiteNodeProperties() + { + return _internalCheckAndGetProperties(SITE_NODE_PROPERTIES, Arrays.asList(CONTENT_HANDLER_PROPERTY), Arrays.asList(LISTENER_PROPERTY)); + } + + /** + * Retrieves the properties of the workflow exporter. + * @return these configured properties + */ + protected Properties _getWorkflowProperties() + { + return _internalCheckAndGetProperties(WORKFLOWS_PROPERTIES, Arrays.asList(CONTENT_HANDLER_PROPERTY), Arrays.asList(LISTENER_PROPERTY)); + } + + /** + * Retrieves the properties of the ametys resources exporter. + * @return these configured properties + */ + protected Properties _getResourcesProperties() + { + return _internalCheckAndGetProperties(RESOURCES_PROPERTIES, Arrays.asList(CONTENT_HANDLER_PROPERTY, OUTPUT_STREAM_PROPERTY), Arrays.asList(LISTENER_PROPERTY)); + } + + /** + * Retrieves the properties of the binary property exporter. + * @return these configured properties + */ + protected Properties _getBinaryPropertiesProperties() + { + return _internalCheckAndGetProperties(BINARY_PROPERTIES, Arrays.asList(OUTPUT_STREAM_PROPERTY), Arrays.asList(LISTENER_PROPERTY)); + } + + /** + * Retrieves the properties of the log exporter. + * @return these configured properties + */ + protected Properties _getLogProperties() + { + return _internalCheckAndGetProperties(LOGS_PROPERTIES, Arrays.asList(OUTPUT_STREAM_PROPERTY), Collections.EMPTY_LIST); + } + + // ------- Empty listener methods ----------------------------------------< + /** + * onSiteNodeExportStart listener + * @throws Exception + */ + public void onSiteNodeExportStart() throws Exception + { + // Nothing by default + } + /** + * onSiteNodeExportEnd listener + * @throws Exception + */ + public void onSiteNodeExportEnd() throws Exception + { + // Nothing by default + } + + /** + * onWorkflowsExportStart listener + * @param workflowRefs + * @throws Exception + */ + public void onWorkflowsExportStart(Set workflowRefs) throws Exception + { + // Nothing by default + } + /** + * onWorkflowsExportEnd listener + * @param workflowRefs + * @throws Exception + */ + public void onWorkflowsExportEnd(Set workflowRefs) throws Exception + { + // Nothing by default + } + + /** + * onResourcesExportStart listener + * @param resources + * @throws Exception + */ + public void onResourcesExportStart(Set resources) throws Exception + { + // Nothing by default + } + /** + * onResourcesExportEnd listener + * @param resources + * @throws Exception + */ + public void onResourcesExportEnd(Set resources) throws Exception + { + // Nothing by default + } + + /** + * onBinaryPropertiesExportStart listener + * @param binaryMap + * @throws Exception + */ + public void onBinaryPropertiesExportStart(Map> binaryMap) throws Exception + { + // Nothing by default + } + /** + * onBinaryPropertiesExportEnd listener + * @param binaryMap + * @throws Exception + */ + public void onBinaryPropertiesExportEnd(Map> binaryMap) throws Exception + { + // Nothing by default + } + + /** + * onLogExportStart listener + * @param externalReferences + * @throws Exception + */ + public void onLogExportStart(Map> externalReferences) throws Exception + { + // Nothing by default + } + /** + * onLogExportEnd listener + * @param externalReferences + * @throws Exception + */ + public void onLogExportEnd(Map> externalReferences) throws Exception + { + // Nothing by default + } + +} Index: main/plugin-web/src/org/apache/jackrabbit/core/ProtectedItemModifier.java =================================================================== --- main/plugin-web/src/org/apache/jackrabbit/core/ProtectedItemModifier.java (revision 0) +++ main/plugin-web/src/org/apache/jackrabbit/core/ProtectedItemModifier.java (revision 0) @@ -0,0 +1,194 @@ +/* + * 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. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.retention.RetentionManagerImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.acl.ACLEditor; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import org.ametys.cms.io.importers.jcr.versions.VersionImporterModifier; + +/** + * ProtectedItemModifier: An abstract helper class to allow classes + * residing outside of the core package to modify and remove protected items. + * The protected item definitions are required in order not to have security + * relevant content being changed through common item operations but forcing + * the usage of the corresponding APIs, which assert that implementation + * specific constraints are not violated. + */ +public abstract class ProtectedItemModifier { + + private static final int DEFAULT_PERM_CHECK = -1; + private final int permission; + + protected ProtectedItemModifier() { + this(DEFAULT_PERM_CHECK); + } + + protected ProtectedItemModifier(int permission) { + Class cl = getClass(); + if (!(UserManagerImpl.class.isAssignableFrom(cl) || + RetentionManagerImpl.class.isAssignableFrom(cl) || + ACLEditor.class.isAssignableFrom(cl) || + org.apache.jackrabbit.core.security.authorization.principalbased.ACLEditor.class.isAssignableFrom(cl) || + VersionImporterModifier.class.isAssignableFrom(cl))) { + throw new IllegalArgumentException("Only UserManagerImpl, RetentionManagerImpl and ACLEditor may extend from the ProtectedItemModifier"); + } + this.permission = permission; + } + + protected NodeImpl addNode(NodeImpl parentImpl, Name name, Name ntName) throws RepositoryException { + return addNode(parentImpl, name, ntName, null); + } + + protected NodeImpl addNode(NodeImpl parentImpl, Name name, Name ntName, NodeId nodeId) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(true, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + + NodeTypeImpl nodeType = parentImpl.sessionContext.getNodeTypeManager().getNodeType(ntName); + org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl def = parentImpl.getApplicableChildNodeDefinition(name, ntName); + + // check for name collisions + // TODO: improve. copied from NodeImpl + NodeState thisState = parentImpl.getNodeState(); + ChildNodeEntry cne = thisState.getChildNodeEntry(name, 1); + if (cne != null) { + // there's already a child node entry with that name; + // check same-name sibling setting of new node + if (!def.allowsSameNameSiblings()) { + throw new ItemExistsException(); + } + // check same-name sibling setting of existing node + NodeId newId = cne.getId(); + NodeImpl n = (NodeImpl) parentImpl.sessionContext.getItemManager().getItem(newId); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException(); + } + } + + return parentImpl.createChildNode(name, nodeType, nodeId); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value value) throws RepositoryException { + return setProperty(parentImpl, name, value, false); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value value, boolean ignorePermissions) throws RepositoryException { + if (!ignorePermissions) { + checkPermission(parentImpl, name, getPermission(false, false)); + } + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue intVs = InternalValue.create(value, parentImpl.sessionContext); + return parentImpl.internalSetProperty(name, intVs); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value[] values) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(false, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue[] intVs = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + intVs[i] = InternalValue.create(values[i], parentImpl.sessionContext); + } + return parentImpl.internalSetProperty(name, intVs); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value[] values, int type) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(false, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue[] intVs = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + intVs[i] = InternalValue.create(values[i], parentImpl.sessionContext); + } + return parentImpl.internalSetProperty(name, intVs, type); + } + + protected void removeItem(ItemImpl itemImpl) throws RepositoryException { + NodeImpl n; + if (itemImpl.isNode()) { + n = (NodeImpl) itemImpl; + } else { + n = (NodeImpl) itemImpl.getParent(); + } + checkPermission(itemImpl, getPermission(itemImpl.isNode(), true)); + // validation: make sure Node is not locked or checked-in. + n.checkSetProperty(); + itemImpl.perform(new ItemRemoveOperation(itemImpl, false)); + } + + protected void markModified(NodeImpl parentImpl) throws RepositoryException { + parentImpl.getOrCreateTransientItemState(); + } + + protected T performProtected(SessionImpl session, SessionOperation operation) throws RepositoryException { + ItemValidator itemValidator = session.context.getItemValidator(); + return itemValidator.performRelaxed(operation, ItemValidator.CHECK_CONSTRAINTS); + } + + private void checkPermission(ItemImpl item, int perm) throws RepositoryException { + if (perm > Permission.NONE) { + SessionImpl sImpl = (SessionImpl) item.getSession(); + AccessManager acMgr = sImpl.getAccessManager(); + + Path path = item.getPrimaryPath(); + acMgr.checkPermission(path, perm); + } + } + + private void checkPermission(NodeImpl node, Name childName, int perm) throws RepositoryException { + if (perm > Permission.NONE) { + SessionImpl sImpl = (SessionImpl) node.getSession(); + AccessManager acMgr = sImpl.getAccessManager(); + + boolean isGranted = acMgr.isGranted(node.getPrimaryPath(), childName, perm); + if (!isGranted) { + throw new AccessDeniedException("Permission denied."); + } + } + } + + private int getPermission(boolean isNode, boolean isRemove) { + if (permission < Permission.NONE) { + if (isNode) { + return (isRemove) ? Permission.REMOVE_NODE : Permission.ADD_NODE; + } else { + return (isRemove) ? Permission.REMOVE_PROPERTY : Permission.SET_PROPERTY; + } + } else { + return permission; + } + } +} \ No newline at end of file Index: main/plugin-web/sitemap-back.xmap =================================================================== --- main/plugin-web/sitemap-back.xmap (revision 19862) +++ main/plugin-web/sitemap-back.xmap (working copy) @@ -170,6 +170,8 @@ + + @@ -192,6 +194,10 @@ + + + + @@ -683,6 +689,28 @@ + + + + + + + + + + + + + + + + + + + + + +