/* * Copyright (C) 2005 Alfresco, Inc. * * Licensed under the Mozilla Public License version 1.1 * with a permitted attribution clause. You may obtain a * copy of the License at * * http://www.alfresco.org/legal/license.txt * * 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.alfresco.web.bean; import java.io.File; import java.io.Serializable; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import javax.transaction.UserTransaction; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.version.VersionModel; import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.version.Version; import org.alfresco.service.cmr.version.VersionType; import org.alfresco.web.app.Application; import org.alfresco.web.app.context.UIContextService; import org.alfresco.web.app.servlet.DownloadContentServlet; import org.alfresco.web.bean.repository.Node; import org.alfresco.web.bean.repository.Repository; import org.alfresco.web.ui.common.Utils; import org.alfresco.web.ui.common.component.UIActionLink; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * @author Kevin Roast */ public class CheckinCheckoutBean { // ------------------------------------------------------------------------------ // Bean property getters and setters /** * @return Returns the BrowseBean. */ public BrowseBean getBrowseBean() { return this.browseBean; } /** * @param browseBean The BrowseBean to set. */ public void setBrowseBean(BrowseBean browseBean) { this.browseBean = browseBean; } /** * @return Returns the NodeService. */ public NodeService getNodeService() { return this.nodeService; } /** * @param nodeService The NodeService to set. */ public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } /** * @return Returns the VersionOperationsService. */ public CheckOutCheckInService getVersionOperationsService() { return this.versionOperationsService; } /** * @param versionOperationsService The VersionOperationsService to set. */ public void setVersionOperationsService(CheckOutCheckInService versionOperationsService) { this.versionOperationsService = versionOperationsService; } /** * @return Returns the ContentService. */ public ContentService getContentService() { return this.contentService; } /** * @param contentService The ContentService to set. */ public void setContentService(ContentService contentService) { this.contentService = contentService; } /** * @return The document node being used for the current operation */ public Node getDocument() { return this.document; } /** * @param document The document node to be used for the current operation */ public void setDocument(Node document) { this.document = document; } /** * @return Returns the working copy Document. */ public Node getWorkingDocument() { return this.workingDocument; } /** * @param workingDocument The working copy Document to set. */ public void setWorkingDocument(Node workingDocument) { this.workingDocument = workingDocument; } /** * Determines whether the document being checked in has * the versionable aspect applied * * @return true if the versionable aspect is applied */ public boolean isVersionable() { return getDocument().hasAspect(ContentModel.ASPECT_VERSIONABLE); } /** * @param keepCheckedOut The keepCheckedOut to set. */ public void setKeepCheckedOut(boolean keepCheckedOut) { this.keepCheckedOut = keepCheckedOut; } /** * @return Returns the keepCheckedOut. */ public boolean getKeepCheckedOut() { return this.keepCheckedOut; } /** * @param minorChange The minorChange to set. */ public void setMinorChange(boolean minorChange) { this.minorChange = minorChange; } /** * @return Returns the minorChange flag. */ public boolean getMinorChange() { return this.minorChange; } /** * @return Returns the version history notes. */ public String getVersionNotes() { return this.versionNotes; } /** * @param versionNotes The version history notes to set. */ public void setVersionNotes(String versionNotes) { this.versionNotes = versionNotes; } /** * @return Returns the selected Space Id. */ public NodeRef getSelectedSpaceId() { return this.selectedSpaceId; } /** * @param selectedSpaceId The selected Space Id to set. */ public void setSelectedSpaceId(NodeRef selectedSpaceId) { this.selectedSpaceId = selectedSpaceId; } /** * @return Returns the copy location. Either the current or other space. */ public String getCopyLocation() { if (this.fileName != null) { return CheckinCheckoutBean.COPYLOCATION_OTHER; } else { return this.copyLocation; } } /** * @param copyLocation The copy location. Either the current or other space. */ public void setCopyLocation(String copyLocation) { this.copyLocation = copyLocation; } /** * @return Returns the message to display when a file has been uploaded */ public String getFileUploadSuccessMsg() { String msg = Application.getMessage(FacesContext.getCurrentInstance(), "file_upload_success"); return MessageFormat.format(msg, new Object[] {getFileName()}); } /** * @return Returns the name of the file */ public String getFileName() { // try and retrieve the file and filename from the file upload bean // representing the file we previously uploaded. FacesContext ctx = FacesContext.getCurrentInstance(); FileUploadBean fileBean = (FileUploadBean)ctx.getExternalContext().getSessionMap(). get(FileUploadBean.FILE_UPLOAD_BEAN_NAME); if (fileBean != null) { this.file = fileBean.getFile(); this.fileName = fileBean.getFileName(); } return this.fileName; } /** * @param fileName The name of the file */ public void setFileName(String fileName) { this.fileName = fileName; // we also need to keep the file upload bean in sync FacesContext ctx = FacesContext.getCurrentInstance(); FileUploadBean fileBean = (FileUploadBean)ctx.getExternalContext().getSessionMap(). get(FileUploadBean.FILE_UPLOAD_BEAN_NAME); if (fileBean != null) { fileBean.setFileName(this.fileName); } } /** * @return Returns the document content used for HTML in-line editing. */ public String getDocumentContent() { return this.documentContent; } /** * @param documentContent The document content for HTML in-line editing. */ public void setDocumentContent(String documentContent) { this.documentContent = documentContent; } /** * @return Returns output from the in-line editor page. */ public String getEditorOutput() { return this.editorOutput; } /** * @param editorOutput The output from the in-line editor page */ public void setEditorOutput(String editorOutput) { this.editorOutput = editorOutput; } // ------------------------------------------------------------------------------ // Navigation action event handlers /** * Action event called by all actions that need to setup a Content Document context on the * CheckinCheckoutBean before an action page/wizard is called. The context will be a Node in * setDocument() which can be retrieved on action pages via getDocument(). * * @param event ActionEvent */ public void setupContentAction(ActionEvent event) { UIActionLink link = (UIActionLink)event.getComponent(); Map params = link.getParameterMap(); String id = params.get("id"); if (id != null && id.length() != 0) { setupContentDocument(id); } else { setDocument(null); } clearUpload(); } /** * Setup a content document node context * * @param id GUID of the node to setup as the content document context * * @return The Node */ private Node setupContentDocument(String id) { if (logger.isDebugEnabled()) logger.debug("Setup for action, setting current document to: " + id); Node node = null; try { // create the node ref, then our node representation NodeRef ref = new NodeRef(Repository.getStoreRef(), id); node = new Node(ref); // create content URL to the content download servlet with ID and expected filename // the myfile part will be ignored by the servlet but gives the browser a hint String url = DownloadContentServlet.generateDownloadURL(ref, node.getName()); node.getProperties().put("url", url); node.getProperties().put("workingCopy", node.hasAspect(ContentModel.ASPECT_WORKING_COPY)); node.getProperties().put("fileType32", Utils.getFileTypeImage(node.getName(), false)); // remember the document setDocument(node); // refresh the UI, calling this method now is fine as it basically makes sure certain // beans clear the state - so when we finish here other beans will have been reset UIContextService.getInstance(FacesContext.getCurrentInstance()).notifyBeans(); } catch (InvalidNodeRefException refErr) { Utils.addErrorMessage(MessageFormat.format(Application.getMessage( FacesContext.getCurrentInstance(), Repository.ERROR_NODEREF), new Object[] {id}) ); } return node; } /** * Action called upon completion of the Check Out file page */ public String checkoutFile() { String outcome = null; UserTransaction tx = null; Node node = getDocument(); if (node != null) { try { tx = Repository.getUserTransaction(FacesContext.getCurrentInstance()); tx.begin(); if (logger.isDebugEnabled()) logger.debug("Trying to checkout content node Id: " + node.getId()); // checkout the node content to create a working copy if (logger.isDebugEnabled()) { logger.debug("Checkout copy location: " + getCopyLocation()); logger.debug("Selected Space Id: " + this.selectedSpaceId); } NodeRef workingCopyRef; if (getCopyLocation().equals(COPYLOCATION_OTHER) && this.selectedSpaceId != null) { // checkout to a arbituary parent Space NodeRef destRef = this.selectedSpaceId; ChildAssociationRef childAssocRef = this.nodeService.getPrimaryParent(destRef); workingCopyRef = this.versionOperationsService.checkout(node.getNodeRef(), destRef, ContentModel.ASSOC_CONTAINS, childAssocRef.getQName()); } else { workingCopyRef = this.versionOperationsService.checkout(node.getNodeRef()); } // set the working copy Node instance Node workingCopy = new Node(workingCopyRef); setWorkingDocument(workingCopy); // create content URL to the content download servlet with ID and expected filename // the myfile part will be ignored by the servlet but gives the browser a hint String url = DownloadContentServlet.generateDownloadURL(workingCopyRef, workingCopy.getName()); workingCopy.getProperties().put("url", url); workingCopy.getProperties().put("fileType32", Utils.getFileTypeImage(workingCopy.getName(), false)); // commit the transaction tx.commit(); // show the page that display the checkout link outcome = "checkoutFileLink"; } catch (Throwable err) { // rollback the transaction try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} Utils.addErrorMessage(Application.getMessage( FacesContext.getCurrentInstance(), MSG_ERROR_CHECKOUT) + err.getMessage(), err); } } else { logger.warn("WARNING: checkoutFile called without a current Document!"); } return outcome; } /** * Action called upon completion of the Check Out file Link download page */ public String checkoutFileOK() { String outcome = null; Node node = getWorkingDocument(); if (node != null) { // clean up and clear action context clearUpload(); setDocument(null); setWorkingDocument(null); outcome = "browse"; } else { logger.warn("WARNING: checkoutFileOK called without a current WorkingDocument!"); } return outcome; } /** * Action called upon completion of the Edit File download page */ public String editFileOK() { String outcome = null; Node node = getDocument(); if (node != null) { // clean up and clear action context clearUpload(); setDocument(null); setWorkingDocument(null); outcome = "browse"; } else { logger.warn("WARNING: editFileOK called without a current Document!"); } return outcome; } /** * Action handler called to calculate which editing screen to display based on the mimetype * of a document. If appropriate, the in-line editing screen will be shown. */ public void editFile(ActionEvent event) { UIActionLink link = (UIActionLink)event.getComponent(); Map params = link.getParameterMap(); String id = params.get("id"); if (id != null && id.length() != 0) { Node node = setupContentDocument(id); // detect the inline editing aspect to see which edit mode to use if (node.hasAspect(ContentModel.ASPECT_INLINEEDITABLE) && node.getProperties().get(ContentModel.PROP_EDITINLINE) != null && ((Boolean)node.getProperties().get(ContentModel.PROP_EDITINLINE)).booleanValue() == true) { // retrieve the content reader for this node ContentReader reader = getContentService().getReader(node.getNodeRef(), ContentModel.PROP_CONTENT); String mimetype = reader.getMimetype(); // calculate which editor screen to display if (MimetypeMap.MIMETYPE_TEXT_PLAIN.equals(mimetype) || MimetypeMap.MIMETYPE_XML.equals(mimetype) || MimetypeMap.MIMETYPE_TEXT_CSS.equals(mimetype)) { // make content available to the editing screen if (reader != null) { setEditorOutput(reader.getContentString()); } else { setEditorOutput(""); } // navigate to appropriate screen FacesContext fc = FacesContext.getCurrentInstance(); fc.getApplication().getNavigationHandler().handleNavigation(fc, null, "editTextInline"); } else { // make content available to the editing screen if (reader != null) { setDocumentContent(reader.getContentString()); } else { setDocumentContent(""); } setEditorOutput(null); // navigate to appropriate screen FacesContext fc = FacesContext.getCurrentInstance(); fc.getApplication().getNavigationHandler().handleNavigation(fc, null, "editHtmlInline"); } } else { // normal downloadable document FacesContext fc = FacesContext.getCurrentInstance(); fc.getApplication().getNavigationHandler().handleNavigation(fc, null, "editFile"); } } } /** * Action handler called to set the content of a node from an inline editing page. */ public String editInlineOK() { String outcome = null; UserTransaction tx = null; Node node = getDocument(); if (node != null) { try { tx = Repository.getUserTransaction(FacesContext.getCurrentInstance()); tx.begin(); if (logger.isDebugEnabled()) logger.debug("Trying to update content node Id: " + node.getId()); // get an updating writer that we can use to modify the content on the current node ContentWriter writer = this.contentService.getWriter(node.getNodeRef(), ContentModel.PROP_CONTENT, true); writer.putContent(this.editorOutput); // commit the transaction tx.commit(); // clean up and clear action context clearUpload(); setDocument(null); setDocumentContent(null); setEditorOutput(null); outcome = "browse"; } catch (Throwable err) { // rollback the transaction try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} Utils.addErrorMessage(Application.getMessage( FacesContext.getCurrentInstance(), MSG_ERROR_UPDATE) + err.getMessage()); } } else { logger.warn("WARNING: editInlineOK called without a current Document!"); } return outcome; } /** * Action to undo the checkout of a document just checked out from the checkout screen. */ public String undoCheckout() { String outcome = null; Node node = getWorkingDocument(); if (node != null) { try { // try to cancel checkout of the working copy this.versionOperationsService.cancelCheckout(node.getNodeRef()); clearUpload(); outcome = "browse"; } catch (Throwable err) { Utils.addErrorMessage(Application.getMessage( FacesContext.getCurrentInstance(), MSG_ERROR_CANCELCHECKOUT) + err.getMessage(), err); } } else { logger.warn("WARNING: undoCheckout called without a current WorkingDocument!"); } return outcome; } /** * Action to undo the checkout of a locked document. This document may either by the original copy * or the working copy node. Therefore calculate which it is, if the working copy is found then * we simply cancel checkout on that document. If the original copy is found then we need to find * the appropriate working copy and perform the action on that node. */ public String undoCheckoutFile() { String outcome = null; Node node = getDocument(); if (node != null) { try { if (node.hasAspect(ContentModel.ASPECT_WORKING_COPY)) { this.versionOperationsService.cancelCheckout(node.getNodeRef()); } else if (node.hasAspect(ContentModel.ASPECT_LOCKABLE)) { // TODO: find the working copy for this document and cancel the checkout on it // is this possible? as currently only the workingcopy aspect has the copyReference // attribute - this means we cannot find out where the copy is to cancel it! // can we construct an XPath node lookup? throw new RuntimeException("NOT IMPLEMENTED"); } else { throw new IllegalStateException("Node supplied for undo checkout has neither Working Copy or Locked aspect!"); } clearUpload(); outcome = "browse"; } catch (Throwable err) { Utils.addErrorMessage(MSG_ERROR_CANCELCHECKOUT + err.getMessage(), err); } } else { logger.warn("WARNING: undoCheckout called without a current WorkingDocument!"); } return outcome; } /** * Action called upon completion of the Check In file page */ public String checkinFileOK() { String outcome = null; UserTransaction tx = null; // NOTE: for checkin the document node _is_ the working document! Node node = getDocument(); if (node != null && (getCopyLocation().equals(COPYLOCATION_CURRENT) || this.getFileName() != null)) { try { tx = Repository.getUserTransaction(FacesContext.getCurrentInstance()); tx.begin(); if (logger.isDebugEnabled()) logger.debug("Trying to checkin content node Id: " + node.getId()); // we can either checkin the content from the current working copy node // which would have been previously updated by the user String contentUrl; String mimetype; if (getCopyLocation().equals(COPYLOCATION_CURRENT)) { ContentData contentData = (ContentData) node.getProperties().get(ContentModel.PROP_CONTENT); contentUrl = (contentData == null ? null : contentData.getContentUrl()); } // or specify a specific file as the content instead else { // add the content to an anonymous but permanent writer location // we can then retrieve the URL to the content to to be set on the node during checkin ContentWriter writer = this.contentService.getWriter(node.getNodeRef(), ContentModel.PROP_CONTENT, false); // TODO: Adjust the mimetype writer.putContent(this.file); contentUrl = writer.getContentUrl(); } if (contentUrl == null || contentUrl.length() == 0) { throw new IllegalStateException("Content URL is empty for specified working copy content node!"); } // add version history text to props Map props = new HashMap(1, 1.0f); props.put(Version.PROP_DESCRIPTION, this.versionNotes); // set the flag for minor or major change if (this.minorChange) { props.put(VersionModel.PROP_VERSION_TYPE, VersionType.MINOR); } else { props.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); } NodeRef originalDoc = this.versionOperationsService.checkin( node.getNodeRef(), props, contentUrl, this.keepCheckedOut); // commit the transaction tx.commit(); // clear action context setDocument(null); clearUpload(); outcome = "browse"; } catch (Throwable err) { // rollback the transaction try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} Utils.addErrorMessage(Application.getMessage( FacesContext.getCurrentInstance(), MSG_ERROR_CHECKIN) + err.getMessage(), err); } } else { logger.warn("WARNING: checkinFileOK called without a current Document!"); } return outcome; } /** * Action called upon completion of the Update File page */ public String updateFileOK() { String outcome = null; UserTransaction tx = null; // NOTE: for update the document node _is_ the working document! Node node = getDocument(); if (node != null && this.getFileName() != null) { try { tx = Repository.getUserTransaction(FacesContext.getCurrentInstance()); tx.begin(); if (logger.isDebugEnabled()) logger.debug("Trying to update content node Id: " + node.getId()); // get an updating writer that we can use to modify the content on the current node ContentWriter writer = this.contentService.getWriter(node.getNodeRef(), ContentModel.PROP_CONTENT, true); writer.putContent(this.file); // commit the transaction tx.commit(); // clear action context setDocument(null); clearUpload(); outcome = "browse"; } catch (Throwable err) { // rollback the transaction try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} Utils.addErrorMessage(Application.getMessage( FacesContext.getCurrentInstance(), MSG_ERROR_UPDATE) + err.getMessage(), err); } } else { logger.warn("WARNING: updateFileOK called without a current Document!"); } return outcome; } /** * Deals with the cancel button being pressed on the check in file page */ public String cancel() { // reset the state clearUpload(); return "browse"; } /** * Clear form state and upload file bean */ private void clearUpload() { // delete the temporary file we uploaded earlier if (this.file != null) { this.file.delete(); } this.file = null; this.fileName = null; this.keepCheckedOut = false; this.minorChange = true; this.copyLocation = COPYLOCATION_CURRENT; this.versionNotes = ""; this.selectedSpaceId = null; // remove the file upload bean from the session FacesContext ctx = FacesContext.getCurrentInstance(); ctx.getExternalContext().getSessionMap().remove(FileUploadBean.FILE_UPLOAD_BEAN_NAME); } // ------------------------------------------------------------------------------ // Private data private static Log logger = LogFactory.getLog(CheckinCheckoutBean.class); /** I18N messages */ private static final String MSG_ERROR_CHECKIN = "error_checkin"; private static final String MSG_ERROR_CANCELCHECKOUT = "error_cancel_checkout"; private static final String MSG_ERROR_UPDATE = "error_update"; private static final String MSG_ERROR_CHECKOUT = "error_checkout"; /** constants for copy location selection */ private static final String COPYLOCATION_CURRENT = "current"; private static final String COPYLOCATION_OTHER = "other"; /** The current document */ private Node document; /** The working copy of the document we are checking out */ private Node workingDocument; /** Content of the document used for HTML in-line editing */ private String documentContent; /** Content of the document returned from in-line editing */ private String editorOutput; /** transient form and upload properties */ private File file; private String fileName; private boolean keepCheckedOut = false; private boolean minorChange = true; private String copyLocation = COPYLOCATION_CURRENT; private String versionNotes = ""; private NodeRef selectedSpaceId = null; /** The BrowseBean to be used by the bean */ private BrowseBean browseBean; /** The NodeService to be used by the bean */ private NodeService nodeService; /** The VersionOperationsService to be used by the bean */ private CheckOutCheckInService versionOperationsService; /** The ContentService to be used by the bean */ private ContentService contentService; }