/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/content/trunk/content-tool/tool/src/java/org/sakaiproject/content/tool/AttachmentAction.java $ * $Id: AttachmentAction.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2008 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.content.tool; import java.util.Collections; import java.util.List; import java.util.Vector; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.cheftool.Context; import org.sakaiproject.cheftool.JetspeedRunData; import org.sakaiproject.cheftool.RunData; import org.sakaiproject.cheftool.VelocityPortlet; import org.sakaiproject.cheftool.VelocityPortletPaneledAction; import org.sakaiproject.cheftool.api.Menu; import org.sakaiproject.cheftool.api.MenuItem; import org.sakaiproject.cheftool.menu.MenuEntry; import org.sakaiproject.cheftool.menu.MenuImpl; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.content.api.ContentCollection; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.content.cover.ContentHostingService; import org.sakaiproject.content.cover.ContentTypeImageService; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.entity.cover.EntityManager; import org.sakaiproject.event.api.SessionState; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.exception.TypeException; import org.sakaiproject.site.cover.SiteService; import org.sakaiproject.util.FileItem; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.Validator; import org.sakaiproject.tool.cover.ToolManager; /** * <p> * AttachmentAction is a helper Action that other tools can use to edit their attachments. * </p> */ public class AttachmentAction { /** Our logger. */ private static Log M_log = LogFactory.getLog(AttachmentAction.class); /** Resource bundle using current language locale */ private static ResourceLoader rb = new ResourceLoader("helper"); /** State attributes for Attachments mode - when it's MODE_DONE the tool can process the results. */ public static final String STATE_MODE = "attachment.mode"; /** State attribute for where there is at least one attachment before invoking attachment tool */ public static final String STATE_HAS_ATTACHMENT_BEFORE = "attachment.has_attachment_before"; /** * State attribute for the Vector of References, one for each attachment. Using tools can pre-populate, and can read the results from here. */ public static final String STATE_ATTACHMENTS = "attachment.attachments"; /** The part of the message after "Attachments for:", set by the client tool. */ public static final String STATE_FROM_TEXT = "attachment.from_text"; /** State attributes for private use. */ private static final String STATE_ATTACHMENT = "attachment.attachment"; /** The collection id being browsed. */ private static final String STATE_BROWSE_COLLECTION_ID = "attachment.collection_id"; /** The id of the "home" collection (can't go up from here. */ private static final String STATE_HOME_COLLECTION_ID = "attachment.collection_home"; /** Modes. */ public static final String MODE_DONE = "done"; public static final String MODE_MAIN = "main"; private static final String MODE_BROWSE = "browse"; private static final String MODE_PROPERTIES = "props"; private static final String MODE_UPLOAD = "upload"; private static final String MODE_URL = "url"; // TODO: path too hard coded /** vm files for each mode. */ private static final String TEMPLATE_MAIN = "helper/chef_attachment_main"; private static final String TEMPLATE_BROWSE = "helper/chef_attachment_browse"; private static final String TEMPLATE_PROPERTIES = "/helper/chef_attachment_properties"; private static final String TEMPLATE_UPLOAD = "helper/chef_attachment_upload"; private static final String TEMPLATE_URL = "helper/chef_attachment_url"; /** the DEFAULT maximun size for file upload */ private static final String FILE_UPLOAD_MAX_SIZE = "file_upload_max_size"; /** * build the context. * * @return The name of the template to use. */ static public String buildHelperContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { // look for a failed upload, which leaves the /special/upload in the URL %%% if (StringUtils.trimToNull(rundata.getParameters().getString("special")) != null) { VelocityPortletPaneledAction.addAlert(state, rb.getFormattedMessage("sizelimitexceeded", new Object[] {state.getAttribute(FILE_UPLOAD_MAX_SIZE)})); } if (state.getAttribute(FILE_UPLOAD_MAX_SIZE) == null) { state.setAttribute(FILE_UPLOAD_MAX_SIZE, ServerConfigurationService.getString("content.upload.max", "1")); } // make sure we have attachments List attachments = (List) state.getAttribute(STATE_ATTACHMENTS); if (attachments == null) { attachments = EntityManager.newReferenceList(); state.setAttribute(STATE_ATTACHMENTS, attachments); } // make sure we have a from text to display if (state.getAttribute(STATE_FROM_TEXT) == null) { state.setAttribute(STATE_FROM_TEXT, ""); } // set me as the helper class state.setAttribute(VelocityPortletPaneledAction.STATE_HELPER, AttachmentAction.class.getName()); // set the "from" message context.put("from", rb.getFormattedMessage("attfor", new Object[] {state.getAttribute(STATE_FROM_TEXT)})); // get the mode String mode = (String) state.getAttribute(STATE_MODE); if (mode == null) mode = MODE_MAIN; if (mode.equals(MODE_MAIN)) { return buildMainContext(portlet, context, rundata, state); } else if (mode.equals(MODE_BROWSE)) { return buildBrowseContext(portlet, context, rundata, state); } else if (mode.equals(MODE_PROPERTIES)) { return buildPropertiesContext(portlet, context, rundata, state); } else if (mode.equals(MODE_UPLOAD)) { return buildUploadContext(portlet, context, rundata, state); } else if (mode.equals(MODE_URL)) { return buildUrlContext(portlet, context, rundata, state); } else { // %%% } return null; } // buildHelperContext /** * build the context for the main display * * @return The name of the template to use. */ static public String buildMainContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { // place the attribute vector (of References) into the context List attachments = (List) state.getAttribute(STATE_ATTACHMENTS); context.put("thelp", rb); context.put("attachments", attachments); // make the content type image service available context.put("contentTypeImageService", ContentTypeImageService.getInstance()); // the menu buildMenu(portlet, context, rundata, state, true, (attachments.size() > 0)); // for toolbar context.put("enabled", Boolean.valueOf(true)); context.put("anyattachment", Boolean.valueOf(attachments.size() > 0)); context.put("has_attachment_before", state.getAttribute(STATE_HAS_ATTACHMENT_BEFORE)); return TEMPLATE_MAIN; } // buildMainContext /** * build the context for the browsing for resources display * * @return The name of the template to use. */ static public String buildBrowseContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { context.put("thelp", rb); // make sure the channedId is set String id = (String) state.getAttribute(STATE_BROWSE_COLLECTION_ID); if (id == null) { id = ContentHostingService.getSiteCollection(ToolManager.getCurrentPlacement().getContext()); state.setAttribute(STATE_BROWSE_COLLECTION_ID, id); state.setAttribute(STATE_HOME_COLLECTION_ID, id); } context.put("contentHostingService", ContentHostingService.getInstance()); String collectionDisplayName = null; List members = null; try { // get this collection's display name ContentCollection collection = ContentHostingService.getCollection(id); collectionDisplayName = collection.getProperties().getPropertyFormatted(ResourceProperties.PROP_DISPLAY_NAME); // get the full set of member objects members = collection.getMemberResources(); // sort by display name, ascending Collections.sort(members, ContentHostingService.newContentHostingComparator(ResourceProperties.PROP_DISPLAY_NAME, true)); } catch (IdUnusedException e) { collectionDisplayName = SiteService.getSiteDisplay(ToolManager.getCurrentPlacement().getContext()); members = new Vector(); } catch (TypeException e) { collectionDisplayName = SiteService.getSiteDisplay(ToolManager.getCurrentPlacement().getContext()); members = new Vector(); } catch (PermissionException e) { collectionDisplayName = SiteService.getSiteDisplay(ToolManager.getCurrentPlacement().getContext()); members = new Vector(); } context.put("collectionDisplayName", collectionDisplayName); context.put("collectionMembers", members); context.put("includeUp", Boolean.valueOf(!id.equals(state.getAttribute(STATE_HOME_COLLECTION_ID)))); // place the attribute vector (of References) into the context context.put("attachments", state.getAttribute(STATE_ATTACHMENTS)); // make the content type image service available context.put("contentTypeImageService", ContentTypeImageService.getInstance()); // the menu buildMenu(portlet, context, rundata, state, false, false); context.put("enabled", Boolean.valueOf(false)); context.put("anyattachment", Boolean.valueOf(false)); return TEMPLATE_BROWSE; } // buildBrowseContext /** * build the context for the upload display * * @return The name of the template to use. */ static public String buildUploadContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { context.put("thelp", rb); // the menu buildMenu(portlet, context, rundata, state, false, false); // for toolbar context.put("enabled", Boolean.valueOf(false)); context.put("anyattachment", Boolean.valueOf(false)); return TEMPLATE_UPLOAD; } // buildUploadContext /** * build the context for the url display * * @return The name of the template to use. */ static public String buildUrlContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { context.put("thelp", rb); // the menu buildMenu(portlet, context, rundata, state, false, false); // for toolbar context.put("enabled", Boolean.valueOf(false)); context.put("anyattachment", Boolean.valueOf(false)); return TEMPLATE_URL; } // buildUrlContext /** * Build the menu. */ private static void buildMenu(VelocityPortlet portlet, Context context, RunData rundata, SessionState state, boolean enabled, boolean anyAttachments) { Menu bar = new MenuImpl(portlet, rundata, "AttachmentAction"); String formName = "mainForm"; bar.add(new MenuEntry(rb.getString("locfil"), null, enabled, MenuItem.CHECKED_NA, "doUpload", formName)); bar.add(new MenuEntry(rb.getString("weburl"), null, enabled, MenuItem.CHECKED_NA, "doUrl", formName)); bar.add(new MenuEntry(rb.getString("frores"), null, enabled, MenuItem.CHECKED_NA, "doBrowse", formName)); bar.add(new MenuEntry(rb.getString("remsel"), null, enabled && anyAttachments, MenuItem.CHECKED_NA, "doRemove", formName)); bar.add(new MenuEntry(rb.getString("remall"), null, enabled && anyAttachments, MenuItem.CHECKED_NA, "doRemove_all", formName)); context.put(Menu.CONTEXT_MENU, bar); context.put(Menu.CONTEXT_ACTION, "AttachmentAction"); state.setAttribute(MenuItem.STATE_MENU, bar); } // buildMenu /** * build the context for the properties editing for attachment display * * @return The name of the template to use. */ static public String buildPropertiesContext(VelocityPortlet portlet, Context context, RunData rundata, SessionState state) { // %%% more context setup context.put("thelp", rb); // put in the single Reference for the selected attachment context.put("attachment", state.getAttribute(STATE_ATTACHMENT)); return TEMPLATE_PROPERTIES; } // buildPropertiesContext /** * Remove the state variables used internally, on the way out. */ static private void cleanupState(SessionState state) { state.removeAttribute(STATE_ATTACHMENT); state.removeAttribute(STATE_BROWSE_COLLECTION_ID); state.removeAttribute(STATE_HOME_COLLECTION_ID); state.removeAttribute(STATE_FROM_TEXT); state.removeAttribute(STATE_HAS_ATTACHMENT_BEFORE); state.removeAttribute(VelocityPortletPaneledAction.STATE_HELPER); } // cleanupState /** * Handle the eventSubmit_doSave command to save the edited attachments. */ static public void doSave(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in done mode state.setAttribute(STATE_MODE, MODE_DONE); // clean up state cleanupState(state); } // doSave /** * Handle the eventSubmit_doCancel command to abort the edits. */ static public void doCancel(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in done mode state.setAttribute(STATE_MODE, MODE_DONE); // remove the attachments from the state to indicate that cancel was done state.removeAttribute(STATE_ATTACHMENTS); // clean up state cleanupState(state); } // doCancel /** * Handle the eventSubmit_doBrowse command to go into browse for a resource on the site mode. */ static public void doBrowse(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in browse mode state.setAttribute(STATE_MODE, MODE_BROWSE); } // doBrowse /** * Handle the eventSubmit_doUpload command to go into upload resource mode. */ static public void doUpload(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in upload mode state.setAttribute(STATE_MODE, MODE_UPLOAD); } // doUpload /** * Handle the eventSubmit_doUrl command to go into enter url mode. */ static public void doUrl(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in url mode state.setAttribute(STATE_MODE, MODE_URL); } // doUrl /** * Handle the eventSubmit_doAdd command to add attachments. */ static public void doAdd(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // add to the attachments vector Vector attachments = (Vector) state.getAttribute(STATE_ATTACHMENTS); // see if the user entered a url to add String url = data.getParameters().getString("url"); if (url != null) url = url.trim(); if ((url != null) && (url.length() > 0)) { // if it's missing the transport, add http:// if (url.indexOf("://") == -1) url = "http://" + url; // make a set of properties to add for the new resource ResourcePropertiesEdit props = ContentHostingService.newResourceProperties(); props.addProperty(ResourceProperties.PROP_DISPLAY_NAME, url); props.addProperty(ResourceProperties.PROP_DESCRIPTION, url); // make an attachment resource for this URL try { ContentResource attachment = ContentHostingService.addAttachmentResource(Validator.escapeResourceName(url), // use the url as the name ResourceProperties.TYPE_URL, url.getBytes(), props); // add a dereferencer for this to the attachments attachments.add(EntityManager.newReference(attachment.getReference())); } catch (Exception any) { M_log.warn("AttachmentAction" + ".doAdd: exception adding attachment resource (urlName: " + Validator.escapeResourceName(url) + "): " + any.toString()); } } // if ((url != null) && (url.length() > 0)) // see if the user uploaded a file FileItem file = data.getParameters().getFileItem("file"); if (file != null) { // the file content byte[] byte[] in = file.get(); // the content type String contentType = file.getContentType(); // the file name - as reported by the browser String browserFileName = file.getFileName(); // we just want the file name part - strip off any drive and path stuff String name = Validator.getFileName(browserFileName); String resourceId = Validator.escapeResourceName(name); // make a set of properties to add for the new resource ResourcePropertiesEdit props = ContentHostingService.newResourceProperties(); props.addProperty(ResourceProperties.PROP_DISPLAY_NAME, name); props.addProperty(ResourceProperties.PROP_DESCRIPTION, browserFileName); // make an attachment resource for this URL try { ContentResource attachment = ContentHostingService.addAttachmentResource(resourceId, contentType, in, props); // add a dereferencer for this to the attachments attachments.add(EntityManager.newReference(attachment.getReference())); } catch (Exception any) { M_log.warn("AttachmentAction" + ".doAdd: exception adding attachment resource (fileName: " + name + "): " + any.toString()); } } // if (file!= null) // if there is at least one attachment if (attachments.size() > 0) { state.setAttribute(STATE_HAS_ATTACHMENT_BEFORE, Boolean.TRUE); } // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doAdd /** * Handle the eventSubmit_doRemove command to remove the selected attachment(s). */ static public void doRemove(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // modify the attachments vector Vector attachments = (Vector) state.getAttribute(STATE_ATTACHMENTS); // read the form to figure out which attachment(s) to remove. String[] selected = data.getParameters().getStrings("select"); // if nothing selected, and there's just one attachment, remove it if (selected == null) { if (attachments.size() == 1) { attachments.clear(); } else { // leave a message state.setAttribute(VelocityPortletPaneledAction.STATE_MESSAGE, rb.getString("alert")); } } else { // run through these 1 based indexes backwards, so we can remove each without invalidating the rest // ASSUME: they are in ascending order for (int i = selected.length - 1; i >= 0; i--) { try { int index = Integer.parseInt(selected[i]) - 1; attachments.removeElementAt(index); } catch (Exception e) { M_log.warn("AttachmentAction" + ".doRemove(): processing selected [" + i + "] : " + e.toString()); } } } // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doRemove /** * Handle the eventSubmit_doRemove_all command to remove the selected attachment(s). */ static public void doRemove_all(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // modify the attachments vector Vector attachments = (Vector) state.getAttribute(STATE_ATTACHMENTS); attachments.clear(); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doRemove_all /** * Handle the eventSubmit_doProperties command to edit the selected attachment's properties. Note: not yet used. */ static public void doProperties(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // modify the attachments vector Vector attachments = (Vector) state.getAttribute(STATE_ATTACHMENTS); // read the form to figure out which %%% attachment from the vector to edit. state.setAttribute(STATE_ATTACHMENT, null); // end up in properties mode state.setAttribute(STATE_MODE, MODE_PROPERTIES); } // doProperties /** * Handle the eventSubmit_doCancel_browse command to abort the browse. */ static public void doCancel_browse(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // clean up any browse state state.removeAttribute(STATE_BROWSE_COLLECTION_ID); state.removeAttribute(STATE_HOME_COLLECTION_ID); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doCancel_browse /** * Handle the eventSubmit_doCancel_add command to abort an add. */ static public void doCancel_add(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doCancel_add /** * Handle the eventSubmit_doBrowse_option command to process inputs from the browse form: go up, go down, or be done. */ static public void doBrowse_option(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // which option was choosen? String option = data.getParameters().getString("option"); if ("cancel".equals(option)) { doCancel_add(data); } else { // read the form / state to figure out which attachment(s) to add. String[] ids = data.getParameters().getStrings("selectedMembers"); Vector idVector = new Vector(); if ((ids != null) && (ids.length > 0)) { for (int index = 0; index < ids.length; index++) { idVector.add(ids[index]); } } updateAttachments(state, idVector); // if there is at least one resource chosed to be added if (idVector.size() > 0) { state.setAttribute(STATE_HAS_ATTACHMENT_BEFORE, Boolean.TRUE); } if ("up".equals(option)) { // get the current collection String id = (String) state.getAttribute(STATE_BROWSE_COLLECTION_ID); // if we are at the home collection, we go no up-er if (id.equals(state.getAttribute(STATE_HOME_COLLECTION_ID))) return; // get the containing collection String containingId = ContentHostingService.getContainingCollectionId(id); // make sure the user can read that if (ContentHostingService.allowGetCollection(containingId)) { state.setAttribute(STATE_BROWSE_COLLECTION_ID, containingId); } else { state.setAttribute(VelocityPortletPaneledAction.STATE_MESSAGE, rb.getString("alert2")); } // end up in browse mode state.setAttribute(STATE_MODE, MODE_BROWSE); } else if ("down".equals(option)) { // get the collection id to move to String id = data.getParameters().getString("itemId"); // make sure the user can read that if (ContentHostingService.allowGetCollection(id)) { state.setAttribute(STATE_BROWSE_COLLECTION_ID, id); } else { state.setAttribute(VelocityPortletPaneledAction.STATE_MESSAGE, rb.getString("alert2")); } // end up in browse mode state.setAttribute(STATE_MODE, MODE_BROWSE); } else // done { // clean up any browse state state.removeAttribute(STATE_BROWSE_COLLECTION_ID); state.removeAttribute(STATE_HOME_COLLECTION_ID); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } } } // doBrowse_option /** * Update the attachments list based on which ids were selected at this browse level. */ static private void updateAttachments(SessionState state, Vector ids) { String id = (String) state.getAttribute(STATE_BROWSE_COLLECTION_ID); List attachments = (List) state.getAttribute(STATE_ATTACHMENTS); List members = null; try { // get the set of member ids ContentCollection collection = ContentHostingService.getCollection(id); members = collection.getMembers(); // for each member for (int i = 0; i < members.size(); i++) { String memberId = (String) members.get(i); String ref = ContentHostingService.getReference(memberId); // if the member id is in the list of ids selected if (ids.contains(memberId)) { // make sure it is in the attachments if (!attachments.contains(ref)) { attachments.add(EntityManager.newReference(ref)); } } else { // make sure its NOT in the attachments attachments.remove(ref); } } } catch (IdUnusedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (TypeException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (PermissionException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // updateAttachments /** * Handle the eventSubmit_doCancel_properties command to abort the properties edit. Note: not yet used. */ static public void doCancel_properties(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // clean up any properties state %%% state.removeAttribute(STATE_ATTACHMENT); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doCancel_properties /** * Handle the eventSubmit_doUpdate_properties command to keep the edited properties. Note: not yet used. */ static public void doUpdate_properties(RunData data) { SessionState state = ((JetspeedRunData) data).getPortletSessionState(((JetspeedRunData) data).getJs_peid()); // modify the attachment Reference Reference attachment = (Reference) state.getAttribute(STATE_ATTACHMENT); // read the form / state to get the %%% changes and make them // clean up any properties state %%% state.removeAttribute(STATE_ATTACHMENT); // end up in main mode state.setAttribute(STATE_MODE, MODE_MAIN); } // doUpdate_properties /** * Dispatch function for upload attachment page */ static public void doDispatch_attachment_upload(RunData data) { String option = data.getParameters().getString("option"); if (option.equalsIgnoreCase("cancel")) { // cancel doCancel_add(data); } else if (option.equalsIgnoreCase("attach")) { // upload doAdd(data); } } // doDispatch_attachment_upload } // AttachmentAction