From ef2c1e6cfeb3b6b57ca15d9beea81a3db3d23ca9 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Mon, 18 May 2015 12:59:53 +0100 Subject: [PATCH 01/14] SAK-21872: Citations copy/duplicate doesn't work as expected. --- .../sakaiproject/citation/impl/BaseCitationService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/citations/citations-impl/impl/src/java/org/sakaiproject/citation/impl/BaseCitationService.java b/citations/citations-impl/impl/src/java/org/sakaiproject/citation/impl/BaseCitationService.java index 8a3b03890466..a09d749653c3 100644 --- a/citations/citations-impl/impl/src/java/org/sakaiproject/citation/impl/BaseCitationService.java +++ b/citations/citations-impl/impl/src/java/org/sakaiproject/citation/impl/BaseCitationService.java @@ -2796,7 +2796,7 @@ protected void copy(BasicCitationCollection other) this.m_comparator = new MultipleKeyComparator( TITLE_AS_KEY, true ); } - set(other); + set(other, true); } @@ -3181,7 +3181,7 @@ protected void checkForUpdates() } else { - set((BasicCitationCollection) edit); + set((BasicCitationCollection) edit, false); } } @@ -3191,7 +3191,7 @@ protected void checkForUpdates() * copy * @param other */ - protected void set(BasicCitationCollection other) + protected void set(BasicCitationCollection other, boolean isTemporary) { this.m_description = other.m_description; // this.m_comparator = other.m_comparator; @@ -3220,7 +3220,7 @@ protected void set(BasicCitationCollection other) { newCitation.copy(oldCitation); newCitation.m_id = oldCitation.m_id; - newCitation.m_temporary = false; + newCitation.m_temporary = isTemporary; this.saveCitation(newCitation); this.add(newCitation); } From 04849d948b98fbcf83c06fafd16eafe3f6db63c7 Mon Sep 17 00:00:00 2001 From: Matthew Buckett Date: Mon, 11 May 2015 10:26:19 +0100 Subject: [PATCH 02/14] =?UTF-8?q?SAK-29355=20Keep=20=E2=80=9Cbad=E2=80=9D?= =?UTF-8?q?=20URL=20in=20error=20site.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows a tool running in the error site to know the URL that the user was having problems accessing, rather than have all the URLs just include the error site ID. A simpler option would have been to use the Sakai session but this would have meant error handling wouldn't have with multiple windows and it would have bloated the size of the session. --- .../portal/api/PortalService.java | 5 + .../portal/charon/SkinnableCharonPortal.java | 44 ++++--- .../portal/charon/handlers/SiteHandler.java | 113 ++++++++++-------- .../service/SiteNeighbourhoodServiceImpl.java | 20 +++- .../pack/src/webapp/WEB-INF/components.xml | 1 + 5 files changed, 113 insertions(+), 70 deletions(-) diff --git a/portal/portal-api/api/src/java/org/sakaiproject/portal/api/PortalService.java b/portal/portal-api/api/src/java/org/sakaiproject/portal/api/PortalService.java index b84b950b00d4..dbf8e1f99c6d 100644 --- a/portal/portal-api/api/src/java/org/sakaiproject/portal/api/PortalService.java +++ b/portal/portal-api/api/src/java/org/sakaiproject/portal/api/PortalService.java @@ -72,6 +72,11 @@ public interface PortalService */ public static final String SAKAI_CONTROLLING_PORTAL = "sakai-controlling-portal"; + /** + * The Site ID that the user was originally trying to access when they hit the error. + */ + String SAKAI_PORTAL_ORIGINAL_SITEID = "SAKAI_PORTAL_ORIGINAL_SITEID"; + /** * ste the state of the portal reset flag. * diff --git a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java index e8fde0d119f0..de43f7ffcf98 100644 --- a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java +++ b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/SkinnableCharonPortal.java @@ -62,7 +62,6 @@ import org.sakaiproject.portal.api.PortalHandler; import org.sakaiproject.portal.api.PortalRenderContext; import org.sakaiproject.portal.api.PortalRenderEngine; -import org.sakaiproject.portal.api.PortalService; import org.sakaiproject.portal.api.PortalSiteHelper; import org.sakaiproject.portal.api.SiteNeighbourhoodService; import org.sakaiproject.portal.api.SiteView; @@ -132,6 +131,7 @@ import au.com.flyingkite.mobiledetect.UAgentInfo; import org.apache.commons.lang.ArrayUtils; + /** *

Charon is the Sakai Site based portal. *

@@ -273,8 +273,13 @@ public void doError(HttpServletRequest req, HttpServletResponse res, Session ses { case ERROR_SITE: { - siteHandler.doSite(req, res, session, "!error", null, null, null, null, - req.getContextPath() + req.getServletPath()); + // This preseves the "bad" origin site ID. + String[] parts = getParts(req); + if (parts.length >= 3) { + String siteId = parts[2]; + ThreadLocalManager.set(PortalService.SAKAI_PORTAL_ORIGINAL_SITEID, siteId); + } + siteHandler.doGet(parts, req, res, session, "!error"); break; } case ERROR_WORKSITE: @@ -818,19 +823,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) // recognize what to do from the path String option = URLUtils.getSafePathInfo(req); - //FindBugs thinks this is not used but is passed to the portal handler - String[] parts = {}; - - if (option == null || "/".equals(option)) - { - // Use the default handler prefix - parts = new String[]{"", handlerPrefix}; - } - else - { - //get the parts (the first will be "") - parts = option.split("/"); - } + String[] parts = getParts(req); Map handlerMap = portalService.getHandlerMap(this); @@ -900,6 +893,25 @@ protected void doGet(HttpServletRequest req, HttpServletResponse res) } } + private String[] getParts(HttpServletRequest req) { + + String option = URLUtils.getSafePathInfo(req); + //FindBugs thinks this is not used but is passed to the portal handler + String[] parts = {}; + + if (option == null || "/".equals(option)) + { + // Use the default handler prefix + parts = new String[]{"", handlerPrefix}; + } + else + { + //get the parts (the first will be "") + parts = option.split("/"); + } + return parts; + } + public void doLogin(HttpServletRequest req, HttpServletResponse res, Session session, String returnPath, boolean skipContainer) throws ToolException { diff --git a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/SiteHandler.java b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/SiteHandler.java index 2f5ff34eccca..66e7245365d0 100644 --- a/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/SiteHandler.java +++ b/portal/portal-impl/impl/src/java/org/sakaiproject/portal/charon/handlers/SiteHandler.java @@ -154,60 +154,15 @@ public int doGet(String[] parts, HttpServletRequest req, HttpServletResponse res { // This is part of the main portal so we simply remove the attribute session.setAttribute(PortalService.SAKAI_CONTROLLING_PORTAL, null); + // site might be specified + String siteId = null; + if (parts.length >= 3) + { + siteId = parts[2]; + } try { - // site might be specified - String siteId = null; - if (parts.length >= 3) - { - siteId = parts[2]; - } - - // recognize an optional page/pageid - String pageId = null; - String toolId = null; - - // may also have the tool part, so check that length is 5 or greater. - if ((parts.length >= 5) && (parts[3].equals("page"))) - { - pageId = parts[4]; - } - - // Tool resetting URL - clear state and forward to the real tool - // URL - // /portal/site/site-id/tool-reset/toolId - // 0 1 2 3 4 - if ((siteId != null) && (parts.length == 5) && (parts[3].equals("tool-reset"))) - { - toolId = parts[4]; - String toolUrl = req.getContextPath() + "/site/" + siteId + "/tool" - + Web.makePath(parts, 4, parts.length); - String queryString = Validator.generateQueryString(req); - if (queryString != null) - { - toolUrl = toolUrl + "?" + queryString; - } - portalService.setResetState("true"); - res.sendRedirect(toolUrl); - return RESET_DONE; - } - - // may also have the tool part, so check that length is 5 or greater. - if ((parts.length >= 5) && (parts[3].equals("tool"))) - { - toolId = parts[4]; - } - - String commonToolId = null; - - if(parts.length == 4) - { - commonToolId = parts[3]; - } - - doSite(req, res, session, siteId, pageId, toolId, commonToolId, parts, - req.getContextPath() + req.getServletPath()); - return END; + return doGet(parts, req, res, session, siteId); } catch (Exception ex) { @@ -220,6 +175,60 @@ public int doGet(String[] parts, HttpServletRequest req, HttpServletResponse res } } + /** + * This extra method is so that we can pass in a different siteId to the one in the URL. + * @see #doGet(String[], HttpServletRequest, HttpServletResponse, Session, String) + */ + public int doGet(String[] parts, HttpServletRequest req, HttpServletResponse res, + Session session, String siteId) throws IOException, ToolException { + + // recognize an optional page/pageid + String pageId = null; + String toolId = null; + + // may also have the tool part, so check that length is 5 or greater. + if ((parts.length >= 5) && (parts[3].equals("page"))) + { + pageId = parts[4]; + } + + // Tool resetting URL - clear state and forward to the real tool + // URL + // /portal/site/site-id/tool-reset/toolId + // 0 1 2 3 4 + if ((siteId != null) && (parts.length == 5) && (parts[3].equals("tool-reset"))) + { + toolId = parts[4]; + String toolUrl = req.getContextPath() + "/site/" + siteId + "/tool" + + Web.makePath(parts, 4, parts.length); + String queryString = Validator.generateQueryString(req); + if (queryString != null) + { + toolUrl = toolUrl + "?" + queryString; + } + portalService.setResetState("true"); + res.sendRedirect(toolUrl); + return RESET_DONE; + } + + // may also have the tool part, so check that length is 5 or greater. + if ((parts.length >= 5) && (parts[3].equals("tool"))) + { + toolId = parts[4]; + } + + String commonToolId = null; + + if(parts.length == 4) + { + commonToolId = parts[3]; + } + + doSite(req, res, session, siteId, pageId, toolId, commonToolId, parts, + req.getContextPath() + req.getServletPath()); + return END; + } + public void doSite(HttpServletRequest req, HttpServletResponse res, Session session, String siteId, String pageId, String toolId, String commonToolId, String [] parts, String toolContextPath) throws ToolException, diff --git a/portal/portal-service-impl/impl/src/java/org/sakaiproject/portal/service/SiteNeighbourhoodServiceImpl.java b/portal/portal-service-impl/impl/src/java/org/sakaiproject/portal/service/SiteNeighbourhoodServiceImpl.java index 552257580dfc..32ec3b908b95 100644 --- a/portal/portal-service-impl/impl/src/java/org/sakaiproject/portal/service/SiteNeighbourhoodServiceImpl.java +++ b/portal/portal-service-impl/impl/src/java/org/sakaiproject/portal/service/SiteNeighbourhoodServiceImpl.java @@ -40,9 +40,11 @@ import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; +import org.sakaiproject.portal.api.PortalService; import org.sakaiproject.portal.api.SiteNeighbourhoodService; import org.sakaiproject.site.api.Site; import org.sakaiproject.site.api.SiteService; +import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.tool.api.Session; import org.sakaiproject.user.api.Preferences; import org.sakaiproject.user.api.PreferencesService; @@ -68,6 +70,8 @@ public class SiteNeighbourhoodServiceImpl implements SiteNeighbourhoodService private ServerConfigurationService serverConfigurationService; private AliasService aliasService; + + private ThreadLocalManager threadLocalManager; /** Should all site aliases have a prefix */ private boolean useAliasPrefix = false; @@ -527,14 +531,26 @@ public void setUserDirectoryService(UserDirectoryService userDirectoryService) this.userDirectoryService = userDirectoryService; } + public void setThreadLocalManager(ThreadLocalManager threadLocalManager) + { + this.threadLocalManager = threadLocalManager; + } + public String lookupSiteAlias(String id, String context) { + // TODO Constant extraction + if ("/site/!error".equals(id)) { + Object originalId = threadLocalManager.get(PortalService.SAKAI_PORTAL_ORIGINAL_SITEID); + if (originalId instanceof String) { + return (String)originalId; + } + } + List aliases = aliasService.getAliases(id); if (!useSiteAliases) { return null; } - List aliases = aliasService.getAliases(id); - if (aliases.size() > 0) + if (aliases.size() > 0) { if (aliases.size() > 1 && log.isInfoEnabled()) { diff --git a/portal/portal-service-impl/pack/src/webapp/WEB-INF/components.xml b/portal/portal-service-impl/pack/src/webapp/WEB-INF/components.xml index e45fc10806de..26893de81d8d 100644 --- a/portal/portal-service-impl/pack/src/webapp/WEB-INF/components.xml +++ b/portal/portal-service-impl/pack/src/webapp/WEB-INF/components.xml @@ -32,6 +32,7 @@ + Date: Wed, 20 May 2015 12:55:01 +0100 Subject: [PATCH 03/14] =?UTF-8?q?SAK-29407=20Don=E2=80=99t=20fail=20where?= =?UTF-8?q?=20params=20undefined.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reports defined earlier may not contain from/to dates so before copying them we should check they aren’t null (to prevent NPE). The copying of null fields is fine in other places. It’s not practical to update the DB to ensure all fields are always defined. --- .../tool/entityproviders/StrippedReportDef.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/entityproviders/StrippedReportDef.java b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/entityproviders/StrippedReportDef.java index 7103611850f3..bb0a752ee66a 100644 --- a/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/entityproviders/StrippedReportDef.java +++ b/sitestats/sitestats-tool/src/java/org/sakaiproject/sitestats/tool/entityproviders/StrippedReportDef.java @@ -43,8 +43,14 @@ public StrippedReportDef(ReportDef reportDef) { this.title = reportDef.getTitle(); this.description = reportDef.getDescription(); ReportParams params = reportDef.getReportParams(); - this.from = params.getWhenFrom().getTime(); - this.to = params.getWhenTo().getTime(); + // The report parameters might not all be defined and so there may not be a + // from / to value. + if (params.getWhenFrom() != null) { + this.from = params.getWhenFrom().getTime(); + } + if (params.getWhenTo() != null) { + this.to = params.getWhenTo().getTime(); + } this.toolIds = params.getWhatToolIds(); this.eventIds = params.getWhatEventIds(); this.resourceIds = params.getWhatResourceIds(); From 9bdab3d626631627355fc9612f7e5335be9f9963 Mon Sep 17 00:00:00 2001 From: Jaques Smith Date: Sat, 11 Apr 2015 13:51:27 -0400 Subject: [PATCH 04/14] KNL-1268 - Extract content file operations to support multiple backends Thanks to OpenCollab ZA for the patch. This makes way for filesystems other than local (or mapped) disk, such as OpenStack Swift. A new interface is added, FileSystemHandler, along with extracting the default file implementation. I made a few fixes to update the patch and clean up a few stylistic things to fit into the kernel code better, but there are no name or design changes. --- .../content/api/FileSystemHandler.java | 43 ++ .../exception/ServerOverloadException.java | 8 + .../webapp/WEB-INF/content-components.xml | 5 + kernel/kernel-impl/pom.xml | 68 +++ .../content/impl/BaseContentService.java | 16 - .../content/impl/ContentServiceSql.java | 6 + .../impl/ContentServiceSqlDefault.java | 8 + .../content/impl/DbContentService.java | 208 +++------ .../impl/DefaultFileSystemHandler.java | 91 ++++ .../content/impl/util/StorageConverter.java | 409 ++++++++++++++++++ kernel/pom.xml | 10 +- 11 files changed, 715 insertions(+), 157 deletions(-) create mode 100644 kernel/api/src/main/java/org/sakaiproject/content/api/FileSystemHandler.java create mode 100644 kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DefaultFileSystemHandler.java create mode 100644 kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/util/StorageConverter.java diff --git a/kernel/api/src/main/java/org/sakaiproject/content/api/FileSystemHandler.java b/kernel/api/src/main/java/org/sakaiproject/content/api/FileSystemHandler.java new file mode 100644 index 000000000000..5488e943f5d4 --- /dev/null +++ b/kernel/api/src/main/java/org/sakaiproject/content/api/FileSystemHandler.java @@ -0,0 +1,43 @@ +package org.sakaiproject.content.api; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This is the api for reading and writing files to some file system. + * + */ +public interface FileSystemHandler { + + /** + * Retrieves an input stream from the file. + * + * @param id The id of the resource. Will not be null or empty. + * @param root The root of the storage. Could be null or empty. + * @param filePath The path to the file. Will not be null or empty. + * @return The valid input stream. Must not be null. + * @throws IOException If the stream could not be created. + */ + public InputStream getInputStream(String id, String root, String filePath) throws IOException; + + /** + * Save the file from the input stream to the path and return the content size. + * + * @param id The id of the resource. Will not be null or empty. + * @param root The root of the storage. Could be null or empty. + * @param filePath The path to save the file to. Will not be null or empty. + * @param stream The stream to read the file from. + * @return The content size. + */ + public long saveInputStream(String id, String root, String filePath, InputStream stream) throws IOException; + + /** + * Delete the file from the path. + * + * @param id The id of the resource. Will not be null or empty. + * @param root The root of the storage. Could be null or empty. + * @param filePath The path to delete. Will not be null or empty. + * @return If the path was deleted. + */ + public boolean delete(String id, String root, String filePath); +} diff --git a/kernel/api/src/main/java/org/sakaiproject/exception/ServerOverloadException.java b/kernel/api/src/main/java/org/sakaiproject/exception/ServerOverloadException.java index dcbcf4e7f29c..cb34fb9ba28a 100644 --- a/kernel/api/src/main/java/org/sakaiproject/exception/ServerOverloadException.java +++ b/kernel/api/src/main/java/org/sakaiproject/exception/ServerOverloadException.java @@ -33,4 +33,12 @@ public ServerOverloadException(String id) { super(id); } + + /** + * Constructor setting exception message and the cause of the exception. + */ + public ServerOverloadException(String message, Throwable t) + { + super(message, t); + } } diff --git a/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml b/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml index c16f935f5ae3..f9b31c49b990 100644 --- a/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml +++ b/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml @@ -18,6 +18,7 @@ class="org.sakaiproject.content.impl.DbContentService" init-method="init" destroy-method="destroy" singleton="true"> + @@ -74,6 +75,10 @@ + + + + diff --git a/kernel/kernel-impl/pom.xml b/kernel/kernel-impl/pom.xml index 8d88824f7453..5a64fc39eacb 100644 --- a/kernel/kernel-impl/pom.xml +++ b/kernel/kernel-impl/pom.xml @@ -387,4 +387,72 @@ --> + + + + storage-convert + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.3.1 + + + test + false + java + + -classpath + + org.sakaiproject.content.impl.util.StorageConverter + + + + + + + + + + diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java index 9d2ae383ba2f..00b70cfd3498 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/BaseContentService.java @@ -23,7 +23,6 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -907,21 +906,6 @@ public void init() userDirectoryService, m_serverConfigurationService); dbNoti.setAction(dropboxNotification); - if (m_bodyPathDeleted != null) { - File deletedFolder = new File(m_bodyPathDeleted); - if (!deletedFolder.exists()) { - if (!deletedFolder.mkdirs()) { - M_log.error("failed to create bodyPathDeleted " + m_bodyPathDeleted + ". Resource backup to file system has been disabled! Please set with the property: bodyPathDeleted@org.sakaiproject.content.api.ContentHostingService"); - m_bodyPathDeleted = null; - } - } - } else { - if (m_bodyPath != null) { - M_log.info("m_bodyPathDeleted is not set as a property. Please set with the property: bodyPathDeleted@org.sakaiproject.content.api.ContentHostingService if you want to allow for deletion of resources"); - } - } - - StringBuilder buf = new StringBuilder(); if (m_bodyVolumes != null) { diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSql.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSql.java index ea154b2e8da6..5128d1420963 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSql.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSql.java @@ -86,6 +86,12 @@ public interface ContentServiceSql */ String getResourceIdXmlSql(); + /** + * returns the sql statement which retrieves all id's and file paths where the file path is not null. + * This is used for converting storage from one FileSystemHandler to another. + */ + public String getResourceIdAndFilePath(); + /** * returns the sql statement which retrieves resource uuid from the content_resource table. */ diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSqlDefault.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSqlDefault.java index 9a3425d24a5a..831349d3a9f8 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSqlDefault.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/ContentServiceSqlDefault.java @@ -123,6 +123,14 @@ public String getResourceIdXmlSql() return "select RESOURCE_ID, XML, BINARY_ENTITY from CONTENT_RESOURCE where FILE_PATH IS NULL"; } + /** + * {@inheritDoc} + */ + public String getResourceIdAndFilePath() + { + return "select RESOURCE_ID, FILE_PATH from CONTENT_RESOURCE where FILE_PATH IS NOT NULL"; + } + /** * returns the sql statement which retrieves the resource uuid from the content_resource table. */ diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DbContentService.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DbContentService.java index 67745066bdcb..0f647c67899f 100644 --- a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DbContentService.java +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DbContentService.java @@ -24,10 +24,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.sql.Blob; @@ -55,6 +51,7 @@ import org.sakaiproject.content.api.ContentCollectionEdit; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.content.api.ContentResourceEdit; +import org.sakaiproject.content.api.FileSystemHandler; import org.sakaiproject.content.api.Lock; import org.sakaiproject.content.api.LockManager; import org.sakaiproject.content.impl.serialize.impl.conversion.Type1BlobCollectionConversionHandler; @@ -169,6 +166,33 @@ public class DbContentService extends BaseContentService * Constructors, Dependencies and their setter methods ************************************************************************************************************************************************/ + /** + * The file system handler to use when files are not stored in the database. + */ + private FileSystemHandler fileSystemHandler = new DefaultFileSystemHandler(); + + /** + * Get the file system handler to use when files are not stored in the database. + *

+ * This can be null if files are stored in the database. + *

+ * The Default is DefaultFileSystemHandler. + */ + public FileSystemHandler getFileSystemHandler(){ + return fileSystemHandler; + } + + /** + * Set the file system handler to use when files are not stored in the database. + *

+ * This can be null if files are stored in the database. + *

+ * The Default is DefaultFileSystemHandler. + */ + public void setFileSystemHandler(FileSystemHandler fileSystemHandler){ + this.fileSystemHandler = fileSystemHandler; + } + /** Dependency: LockManager */ protected LockManager m_lockManager = null; @@ -438,6 +462,12 @@ public void init() convertToFile(); } + //Check that there is a valid file system handler + if (m_bodyPath != null && fileSystemHandler == null) + { + throw new IllegalStateException("There is no FileSystemHandler set for the ContentService!"); + } + M_log.info("init(): tables: " + m_collectionTableName + " " + m_resourceTableName + " " + m_resourceBodyTableName + " " + m_groupTableName + " locks-in-db: " + m_locksInDb + " bodyPath: " + m_bodyPath + " storage: " + m_storage); @@ -1906,16 +1936,7 @@ public void removeDeletedResource(ContentResourceEdit edit) // if we have been configured to use an external file system if (m_bodyPath != null) { - if (m_bodyPathDeleted != null) { - // form the file name - File file = new File(externalResourceFileName(m_bodyPathDeleted, edit)); - - // delete - if (file.exists()) - { - file.delete(); - } - } + delResourceBodyFilesystem(m_bodyPathDeleted, edit); } // otherwise use the database @@ -2101,7 +2122,7 @@ public void removeResource(ContentResourceEdit edit, boolean removeContent) // if we have been configured to use an external file system if (removeContent) { M_log.info("Removing resource ("+edit.getId()+") content: "+m_bodyPath); - delResourceBodyFilesystem(edit); + delResourceBodyFilesystem(m_bodyPath, edit); } else { M_log.info("Removing original resource reference ("+edit.getId()+") without removing the actual content: "+m_bodyPath); } @@ -2290,35 +2311,29 @@ public InputStream streamResourceBody(ContentResource resource) throws ServerOve /** * Return an input stream. * - * @param resource - - * the resource for the stream It is a non-fatal error for the file not to be readible as long as the resource's expected length is - * zero. We check for the body length *after* we try to read the file. If the file - * is readible, we simply read it and return it as the body. + * @param resource the resource to resolve to a stream + * It is a non-fatal error for the file not to be readible as long as the resource's expected length is + * zero. In this case, a null stream is returned. Otherwise, attempt to prepare a stream for reading the + * file body and fail with an exception on error. */ protected InputStream streamResourceBodyFilesystem(String rootFolder, ContentResource resource) throws ServerOverloadException { - // form the file name - File file = new File(externalResourceFileName(rootFolder,resource)); + if (((BaseResourceEdit) resource).m_contentLength == 0) + { + // Zero-length files are not written, so don't bother checking the filesystem. + return null; + } - // read the new try { - FileInputStream in = new FileInputStream(file); - return in; + return fileSystemHandler.getInputStream(((BaseResourceEdit) resource).m_id, rootFolder, ((BaseResourceEdit) resource).m_filePath); } - catch (FileNotFoundException t) + catch (IOException e) { - // If there is not supposed to be data in the file - simply return null - if (((BaseResourceEdit) resource).m_contentLength == 0) - { - return null; - } - // If we have a non-zero body length and reading failed, it is an error worth of note - M_log.warn(": failed to read resource: " + resource.getId() + " len: " + ((BaseResourceEdit) resource).m_contentLength + " : " + t); - throw new ServerOverloadException("failed to read resource body"); - // return null; + M_log.warn("Failed to read resource: " + resource.getId() + " len: " + ((BaseResourceEdit) resource).m_contentLength, e); + throw new ServerOverloadException("Failed to read resource body", e); } } @@ -2453,91 +2468,23 @@ protected boolean putResourceBodyDb(ContentResourceEdit edit, InputStream stream */ private boolean putResourceBodyFilesystem(ContentResourceEdit resource, InputStream stream, String rootFolder) { - // Do not create the files for resources with zero length bodies - if ((stream == null)) return true; - - // form the file name - File file = new File(externalResourceFileName(rootFolder, resource)); - - // delete the old - if (file.exists()) - { - file.delete(); - } - - FileOutputStream out = null; - - // add the new - try - { - // make sure all directories are there - File container = file.getParentFile(); - if (container != null) - { - container.mkdirs(); - } - - // write the file - out = new FileOutputStream(file); - - long byteCount = 0; - // chunk - byte[] chunk = new byte[STREAM_BUFFER_SIZE]; - int lenRead; - while ((lenRead = stream.read(chunk)) != -1) - { - out.write(chunk, 0, lenRead); - byteCount += lenRead; - } - - resource.setContentLength(byteCount); - ResourcePropertiesEdit props = resource.getPropertiesEdit(); - props.addProperty(ResourceProperties.PROP_CONTENT_LENGTH, Long.toString(byteCount)); - if (resource.getContentType() != null) - { - props.addProperty(ResourceProperties.PROP_CONTENT_TYPE, resource.getContentType()); - } - } - // catch (Throwable t) - // { - // M_log.warn(": failed to write resource: " + resource.getId() + " : " + t); - // return false; - // } - catch (IOException e) - { - M_log.warn("IOException", e); - return false; - } - finally - { - if (stream != null) - { - try - { - stream.close(); - } - catch (IOException e) - { - // TODO Auto-generated catch block - M_log.warn("IOException ", e); - } - } - - if (out != null) - { - try - { - out.close(); - } - catch (IOException e) - { - // TODO Auto-generated catch block - M_log.warn("IOException ", e); - } - } - } - - return true; + try + { + long byteCount = fileSystemHandler.saveInputStream(((BaseResourceEdit) resource).m_id, rootFolder, ((BaseResourceEdit) resource).m_filePath, stream); + resource.setContentLength(byteCount); + ResourcePropertiesEdit props = resource.getPropertiesEdit(); + props.addProperty(ResourceProperties.PROP_CONTENT_LENGTH, Long.toString(byteCount)); + if (resource.getContentType() != null) + { + props.addProperty(ResourceProperties.PROP_CONTENT_TYPE, resource.getContentType()); + } + return true; + } + catch (IOException e) + { + M_log.warn("IOException", e); + return false; + } } /** @@ -2585,16 +2532,9 @@ protected void delResourceBodyDb(ContentResourceEdit resource, String resourceBo * @param resource * The resource whose body is being written. */ - protected void delResourceBodyFilesystem(ContentResourceEdit resource) + protected void delResourceBodyFilesystem(String rootFolder, ContentResourceEdit resource) { - // form the file name - File file = new File(externalResourceFileName(m_bodyPath,resource)); - - // delete - if (file.exists()) - { - file.delete(); - } + fileSystemHandler.delete(((BaseResourceEdit) resource).m_id, rootFolder, ((BaseResourceEdit) resource).m_filePath); } public int getMemberCount(String collectionId) @@ -2797,18 +2737,6 @@ public String getResourceStorageFields() } // DbStorage - /** - * Form the full file path+name used to store the resource body in an external file system. - * - * @param resource - * The resource. - * @return The resource external file name. - */ - protected String externalResourceFileName(String rootFolder, ContentResource resource) - { - return rootFolder + ((BaseResourceEdit) resource).m_filePath; - } - @SuppressWarnings("unchecked") public Map getMostRecentUpdate(String id) { diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DefaultFileSystemHandler.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DefaultFileSystemHandler.java new file mode 100644 index 000000000000..029f298d9e9f --- /dev/null +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/DefaultFileSystemHandler.java @@ -0,0 +1,91 @@ +package org.sakaiproject.content.impl; + +import org.sakaiproject.content.api.FileSystemHandler; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.util.FileCopyUtils; + +/** + * The default implementation of FileSystemHandler, targeting local disk. + * + * This class read and writes content files to local filesystem paths. + */ +public class DefaultFileSystemHandler implements FileSystemHandler { + private boolean useIdForFilePath = false; + + /** + * Default constructor. + */ + public DefaultFileSystemHandler() { + } + + /** + * Whether to use the id for the file path. + */ + public void setUseIdForFilePath(boolean useIdForFilePath){ + this.useIdForFilePath = useIdForFilePath; + } + + /** + * A Helper method to get the File object for the parameters. + * This method will look at the property useIdForFilePath to see if the + * id must be used in the file path. + * + * @param id The id of the resource. + * @param root The root of the storage. + * @param filePath The path to save the file to. + * @return The File object. + */ + private File getFile(String id, String root, String filePath){ + if (useIdForFilePath) { + return new File(root, id); + } else { + return new File(root, filePath); + } + } + + @Override + public InputStream getInputStream(String id, String root, String filePath) throws IOException { + return new FileInputStream(getFile(id, root, filePath)); + } + + @Override + public long saveInputStream(String id, String root, String filePath, InputStream stream) throws IOException { + // Do not create the files for resources with zero length bodies + if ((stream == null)) { + return 0L; + } + + // form the file name + File file = getFile(id, root, filePath); + + // delete the old + if (file.exists()) { + file.delete(); + } + + // add the new + // make sure all directories are there + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + + // write the file + return FileCopyUtils.copy(stream, new FileOutputStream(file)); + } + + @Override + public boolean delete(String id, String root, String filePath){ + File file = getFile(id, root, filePath); + + // delete + if (file.exists()) { + return file.delete(); + } + return false; + } +} diff --git a/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/util/StorageConverter.java b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/util/StorageConverter.java new file mode 100644 index 000000000000..a8780f315ebd --- /dev/null +++ b/kernel/kernel-impl/src/main/java/org/sakaiproject/content/impl/util/StorageConverter.java @@ -0,0 +1,409 @@ +package org.sakaiproject.content.impl.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Enumeration; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import javax.sql.DataSource; +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.beanutils.MethodUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.sakaiproject.content.api.FileSystemHandler; +import org.sakaiproject.content.impl.ContentServiceSqlDefault; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; + +/** + * This is a utility class to convert the storage from one FileSystem to + * another. + * + * @author Jaques + */ +public class StorageConverter { + private static final Log log = LogFactory.getLog(StorageConverter.class); + + /** + * The datasource for the database connections. + */ + private DataSource dataSource; + + /** + * The connection to the database. + */ + private Connection connection; + + /** + * The database connection driver. + */ + private String connectionDriver; + + /** + * The database connection URL. + */ + private String connectionURL; + + /** + * The database connection username. + */ + private String connectionUsername; + + /** + * The database connection password. + */ + private String connectionPassword; + + /** + * The sql to retrieve the content id's and paths. + * The id field must be the first field. + * The path field must be the second field. + */ + private String contentSql = new ContentServiceSqlDefault().getResourceIdAndFilePath(); + + /** + * The root body path for the source resources. + */ + private String sourceBodyPath; + + /** + * The root body path for the destination resources. + */ + private String destinationBodyPath; + + /** + * The source file system handler. + */ + private FileSystemHandler sourceFileSystemHandler; + + /** + * The destination file system handler. + */ + private FileSystemHandler destinationFileSystemHandler; + + /** + * Whether to delete the resources from the source. + */ + private boolean deleteFromSource = false; + + /** + * Whether to ignore missing resources. + */ + private boolean ignoreMissing = true; + + /** + * Set the datasource for the database connections. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the connection to the database. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setConnection(Connection connection) { + this.connection = connection; + } + + /** + * Set the database connection driver. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setConnectionDriver(String connectionDriver) { + this.connectionDriver = connectionDriver; + } + + /** + * Set the database connection URL. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setConnectionURL(String connectionURL) { + this.connectionURL = connectionURL; + } + + /** + * Set the database connection username. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setConnectionUsername(String connectionUsername) { + this.connectionUsername = connectionUsername; + } + + /** + * Set the database connection password. + * Either the datasource, connection or the connection details (driver, url, + * username and password) must be set. + */ + public void setConnectionPassword(String connectionPassword) { + this.connectionPassword = connectionPassword; + } + + /** + * Set the sql to retrieve the content id's and paths. + * The id field must be the first field. + * The path field must be the second field. + */ + public void setContentSql(String contentSql) { + this.contentSql = contentSql; + } + + /** + * Set the root body path for the source resources. + */ + public void setSourceBodyPath(String sourceBodyPath) { + this.sourceBodyPath = sourceBodyPath; + } + + /** + * Set the root body path for the destination resources. + */ + public void setDestinationBodyPath(String destinationBodyPath) { + this.destinationBodyPath = destinationBodyPath; + } + + /** + * Set the source file system handler. + */ + public void setSourceFileSystemHandler(FileSystemHandler source) { + this.sourceFileSystemHandler = source; + } + + /** + * Set the destination file system handler. + */ + public void setDestinationFileSystemHandler(FileSystemHandler destination) { + this.destinationFileSystemHandler = destination; + } + + /** + * Set whether to delete the resources from the source. + */ + public void setDeleteFromSource(boolean deleteFromSource) { + this.deleteFromSource = deleteFromSource; + } + + /** + * Setup the datasource. THis method first look for a valid datasource, + * then a connection and lastly will create a datasource from the + * connection details. + */ + private void setupDataSource() throws IllegalStateException { + if (dataSource != null) { + return; + } + if (connection != null) { + dataSource = new SingleConnectionDataSource(connection, false); + return; + } + try { + Class.forName(connectionDriver); + dataSource = new SimpleDriverDataSource(DriverManager.getDriver(connectionURL), connectionURL, connectionUsername, connectionPassword); + } catch (Exception e) { + throw new IllegalStateException("Either a valid datasource, connection or the connection details must be set!", e); + } + } + + /** + * Transfer the resources from the source file system handler to the + * destination. + */ + public void convertStorage() { + log.info("Start converting storage...."); + setupDataSource(); + if (sourceFileSystemHandler == null) { + throw new IllegalStateException("The source FileSystemHandler must be set!"); + } + if (destinationFileSystemHandler == null) { + throw new IllegalStateException("The destination FileSystemHandler must be set!"); + } + final AtomicInteger counter = new AtomicInteger(0); + // read content_resource records that have null file path + JdbcTemplate template = new JdbcTemplate(dataSource); + template.query(contentSql, new RowCallbackHandler() { + public void processRow(ResultSet resultSet) throws SQLException { + counter.incrementAndGet(); + String id = resultSet.getString(1); + String path = resultSet.getString(2); + try { + InputStream input = sourceFileSystemHandler.getInputStream(id, sourceBodyPath, path); + if (input != null) { + destinationFileSystemHandler.saveInputStream(id, destinationBodyPath, path, input); + } + if (deleteFromSource) { + sourceFileSystemHandler.delete(id, sourceBodyPath, path); + } + } catch (IOException e) { + if (ignoreMissing) { + print("Missing file: " + id); + } else { + log.error("Failed to read or write resources from or to the FileSystemHandlers", e); + throw new SQLException("Failed to read or write resources from or to the FileSystemHandlers", e); + } + } + } + }); + log.info("Converted " + counter.get() + " records...."); + log.info("Finished converting storage...."); + } + + public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException{ + print("Checking arguments..."); + if (args == null || args.length == 0 || args[0].contains("help")) { + printHelp(); + return; + } + + Properties p = readProperties(args); + print("Properties: " + p); + StorageConverter sc = new StorageConverter(); + FileSystemHandler sourceFSH = null; + FileSystemHandler destinationFSH = null; + + try { + print("Database connection..."); + sc.setConnectionDriver(p.getProperty("connectionDriver")); + sc.setConnectionURL(p.getProperty("connectionURL")); + sc.setConnectionUsername(p.getProperty("connectionUsername")); + sc.setConnectionPassword(p.getProperty("connectionPassword")); + print("Source FileSystemHandler..."); + sourceFSH = getFileSystemHandler(p, "sourceFileSystemHandler"); + sc.setSourceBodyPath(p.getProperty("sourceBodyPath")); + sc.setSourceFileSystemHandler(sourceFSH); + sc.setDeleteFromSource(Boolean.parseBoolean(p.getProperty("deleteFromSource"))); + print("Destination FileSystemHandler..."); + destinationFSH = getFileSystemHandler(p, "destinationFileSystemHandler"); + sc.setDestinationBodyPath(p.getProperty("destinationBodyPath")); + sc.setDestinationFileSystemHandler(destinationFSH); + + if(p.containsKey("contentSql")){ + sc.setContentSql(p.getProperty("contentSql")); + } + + print("Running convert..."); + sc.convertStorage(); + print("Done..."); + } finally { + destroy(sourceFSH); + destroy(destinationFSH); + } + } + + /** + * Calls the objects destroy method is it exists. + */ + private static void destroy(Object o) throws IllegalAccessException, InvocationTargetException { + if (o == null) return; + print("Destroying " + o + "..."); + try { + print("Check if there is a destroy method..."); + MethodUtils.invokeExactMethod(o, "destroy", (Object[])null); + print("destroy method invoked..."); + } catch (NoSuchMethodException e) { + print("No destroy method..."); + } + } + + /** + * Creates the FileSystemHandler and set all its properties. + * Will also call the init method if it exists. + */ + private static FileSystemHandler getFileSystemHandler(Properties p, String fileSystemHandlerName) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException{ + String clazz = p.getProperty(fileSystemHandlerName); + print("Building FileSystemHandler: " + clazz); + Class fshClass = Class.forName(clazz).asSubclass(FileSystemHandler.class); + FileSystemHandler fsh = fshClass.newInstance(); + + Enumeration propertyNames = (Enumeration) p.propertyNames(); + while (propertyNames.hasMoreElements()) { + String fullProperty = propertyNames.nextElement(); + if (fullProperty.startsWith(fileSystemHandlerName + ".")) { + String property = fullProperty.substring(fullProperty.indexOf(".")+1); + print("Setting property: " + property); + BeanUtils.setProperty(fsh, property, p.getProperty(fullProperty)); + } + } + + try { + print("Check if there is a init method..."); + MethodUtils.invokeExactMethod(fsh, "init", (Object[])null); + print("init method invoked..."); + } catch (NoSuchMethodException e) { + print("No init method..."); + } + print("Done with FileSystemHandler: " + clazz); + return fsh; + } + + /** + * Read the properties file. Return null of the file is not found. + */ + private static Properties readProperties(String[] args) throws IOException { + Properties p = new Properties(){ + + @Override + public String getProperty(String key) { + String prop = super.getProperty(key); + print("- Property " + key + "='" + prop + "'"); + return prop; + } + + }; + for(int i = 0; i < args.length; i++){ + if("-p".equals(args[i])){ + p.load(new FileInputStream(new File(args[++i]))); + } + if(args[i].startsWith("-")){ + p.put(args[i].substring(1), args[++i]); + } + } + return p; + } + + private static void printHelp(){ + print("----------------------------------------------------------------------"); + print("StorageConverter Help"); + print("The StorageConverter needs properties to complete the conversion."); + print("These properties can either be loaded in a properties file indicated with '-p' followed by the location of the properties file"); + print("or the properties specified in the arguments with a leading '-' followed by the values."); + print(""); + print("Properties (mandatory):"); + print("- connectionDriver: The database connection driver class."); + print("- connectionURL: The database connection URL."); + print("- connectionUsername: The database connection username."); + print("- connectionPassword: The database connection password."); + print("- sourceFileSystemHandler: This is the full class name of the source FileSystemHandler."); + print("- sourceFileSystemHandler.: You can set any property on the source FileSystemHandler by referensing their property names."); + print("- sourceBodyPath: The path set in sakai.properties for the source."); + print("- destinationFileSystemHandler: This is the full class name of the destination FileSystemHandler."); + print("- destinationFileSystemHandler.: You can set any property on the destination FileSystemHandler by referensing their property names."); + print("- destinationBodyPath: The path set in sakai.properties for the destination."); + print(""); + print("Properties (optional):"); + print("- deleteFromSource: Whether to delete the source files. Default false."); + print("- contentSql: The sql statement to retrieve the resource id's and paths. Default is new ContentServiceSqlDefault().getResourceIdAndFilePath()"); + print("----------------------------------------------------------------------"); + } + + /** + * Print the text to the screen. + */ + private static void print(String text){ + System.out.println(text); + } +} diff --git a/kernel/pom.xml b/kernel/pom.xml index 709add938ff4..45d61eac9e21 100644 --- a/kernel/pom.xml +++ b/kernel/pom.xml @@ -511,7 +511,15 @@ - + + org.codehaus.mojo + exec-maven-plugin + 1.3.1 + + java + true + + true org.sakaiproject.maven.plugins From 8ade420be4df721b0b851143ef67a0f48dad72cd Mon Sep 17 00:00:00 2001 From: Noah Botimer Date: Mon, 25 May 2015 16:16:18 -0400 Subject: [PATCH 05/14] KNL-1268 - Use bean alias to select default content filesystem --- .../src/main/webapp/WEB-INF/content-components.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml b/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml index f9b31c49b990..ffcb8c3b2472 100644 --- a/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml +++ b/kernel/kernel-component/src/main/webapp/WEB-INF/content-components.xml @@ -18,7 +18,7 @@ class="org.sakaiproject.content.impl.DbContentService" init-method="init" destroy-method="destroy" singleton="true"> - + @@ -79,6 +79,10 @@ + + + + From 548ad32595ec478796fda3effa05f6be38e3aa54 Mon Sep 17 00:00:00 2001 From: Brian Baillargeon Date: Mon, 25 May 2015 16:41:41 -0400 Subject: [PATCH 06/14] SAK-29418: In an assignment resubmission, removing all attachments should be considered a modification --- .../assignment/tool/AssignmentAction.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/assignment/assignment-tool/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java b/assignment/assignment-tool/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java index 19c28483d428..0c6fb49f1c70 100644 --- a/assignment/assignment-tool/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java +++ b/assignment/assignment-tool/tool/src/java/org/sakaiproject/assignment/tool/AssignmentAction.java @@ -1520,12 +1520,12 @@ protected String build_student_view_submission_context(VelocityPortlet portlet, // the attachments from the previous submission List submittedAttachments = s.getSubmittedAttachments(); - newAttachments = areAttachmentsNew(submittedAttachments, currentAttachments); + newAttachments = areAttachmentsModified(submittedAttachments, currentAttachments); } else { // There is no previous submission, attachments are modified if anything has been uploaded - newAttachments = currentAttachments != null && !currentAttachments.isEmpty(); + newAttachments = CollectionUtils.isNotEmpty(currentAttachments); } // put the resubmit information into context @@ -1615,17 +1615,20 @@ protected String build_student_view_submission_context(VelocityPortlet portlet, } // build_student_view_submission_context /** - * Determines if there are new attachments - * @return true if currentAttachments is not empty and isn't equal to oldAttachments + * Determines if the attachments have been modified + * @return true if currentAttachments isn't equal to oldAttachments */ - private boolean areAttachmentsNew(List oldAttachments, List currentAttachments) + private boolean areAttachmentsModified(List oldAttachments, List currentAttachments) { - if (currentAttachments == null || currentAttachments.isEmpty()) + boolean hasCurrent = CollectionUtils.isNotEmpty(currentAttachments); + boolean hasOld = CollectionUtils.isNotEmpty(oldAttachments); + + if (!hasCurrent) { //there are no current attachments - return false; + return hasOld; } - if (oldAttachments == null || oldAttachments.isEmpty()) + if (!hasOld) { //there are no old attachments (and there are new ones) return true; From 94798a402e2ce29407c73ba08bfdef6197ae556f Mon Sep 17 00:00:00 2001 From: Brian Baillargeon Date: Mon, 25 May 2015 17:07:44 -0400 Subject: [PATCH 07/14] SAK-29419: stuviewsubm.reminder needs an exclamation mark for consistency --- assignment/assignment-bundles/resources/assignment.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment/assignment-bundles/resources/assignment.properties b/assignment/assignment-bundles/resources/assignment.properties index bbdb63bf82b8..5f7668234046 100644 --- a/assignment/assignment-bundles/resources/assignment.properties +++ b/assignment/assignment-bundles/resources/assignment.properties @@ -423,7 +423,7 @@ stuviewsubm.attfromserverlabelmore= or select more files from workspace or site feedbacktext = Feedback Text stuviewsubm.submitreminder=Don't forget to save or submit! stuviewsubm.modifytoresubmit=You may resubmit if you modify this submission. -stuviewsubm.reminder=Don't forget to submit +stuviewsubm.reminder=Don't forget to submit! feedbackcomment = Feedback Comment stuviewsubm.typesubhaschanged.inline = The submission type for this assignment has changed. This text from your previous submission is for your reference only and will NOT be included if you resubmit. From 92036948bf4e16b0ab16de1486cb30dd2c98a3be Mon Sep 17 00:00:00 2001 From: Charles Hedrick Date: Tue, 26 May 2015 10:47:05 -0400 Subject: [PATCH 08/14] LSNBLDR-496; internationalize LEAP changes --- .../tool/producers/ShowPageProducer.java | 3 ++ .../tool/src/resources/messages.properties | 10 +++++++ .../tool/src/webapp/templates/ShowPage.html | 28 ++++++------------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/tool/producers/ShowPageProducer.java b/lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/tool/producers/ShowPageProducer.java index 4562f3d518d4..a4105b2d4fc3 100644 --- a/lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/tool/producers/ShowPageProducer.java +++ b/lessonbuilder/tool/src/java/org/sakaiproject/lessonbuildertool/tool/producers/ShowPageProducer.java @@ -691,6 +691,8 @@ public void fillComponents(UIContainer tofill, ViewParameters viewParams, Compon simplePageBean.adjustBackPath(params.getBackPath(), currentPage.getPageId(), pageItem.getId(), pageItem.getName()); } + UIOutput.make(tofill, "actionmenu").decorate(new UIFreeAttributeDecorator("aria-label", messageLocator.getMessage("simplepage.menubar.aria"))); + // put out link to index of pages GeneralViewParameters showAll = new GeneralViewParameters(PagePickerProducer.VIEW_ID); showAll.setSource("summary"); @@ -3207,6 +3209,7 @@ public int checkIEVersion() { private void createToolBar(UIContainer tofill, SimplePage currentPage, boolean isStudent) { UIBranchContainer toolBar = UIBranchContainer.make(tofill, "tool-bar:"); + toolBar.decorate(new UIFreeAttributeDecorator("aria-label", messageLocator.getMessage("simplepage.toolbar.aria"))); boolean studentPage = currentPage.getOwner() != null; // toolbar diff --git a/lessonbuilder/tool/src/resources/messages.properties b/lessonbuilder/tool/src/resources/messages.properties index 3ea61e631665..7d8910fda3f2 100644 --- a/lessonbuilder/tool/src/resources/messages.properties +++ b/lessonbuilder/tool/src/resources/messages.properties @@ -1,4 +1,5 @@ simplepage.text=Add Text +simplepage.text.uclabel=TEXT simplepage.text.tooltip=Add a box that can contain text and other web content simplepage.instructions=You have not yet created this simple page, click 'Edit page' to begin. simplepage.not_available=This page has not yet been created by the course instructor or you do not have the necessary permissions to read it @@ -11,6 +12,8 @@ simplepage.adding-text=Adding text to: simplepage.continue=Continue simplepage.toolbar=Editing tools +simplepage.toolbar.aria=content tools +simplepage.menubar.aria=page functions simplepage.maincontent=Main page content simplepage.notreleased=You have checked the 'Hide until release date' box in the Settings window, and the date has not arrived. Although the page will appear in the margin, users will not be able to use it yet. simplepage.notreleased.text=Note: This page will not be available to standard users until {}. You can change this in the Settings window. @@ -109,6 +112,7 @@ simplepage.resource=Add Content Link simplepage.resource.tooltip=Add link: Upload file or use existing file in Resources tool and add link to it, or enter a URL for another site simplepage.resource-descrip=Add link: Upload file or use existing file in Resources tool and add link to it, or enter a URL for another site simplepage.subpage=Add Subpage +simplepage.subpage.uclabel=SUBPAGE simplepage.subpage-next=Next page, i.e. page replaces the current one rather than returning to the current one simplepage.subpage-button=Show as button rather than link simplepage.subpage.tooltip=Create a new page and add a link to it here @@ -165,6 +169,7 @@ simplepage.additional-website-instructions=instructions/website.html simplepage.general-instructions=instructions/general.html simplepage.help=Help +simplepage.tips.uclabel=TIPS simplepage.noitems_error_admin=We suggest that you start out by using Add Text to put something on this page. Then you can add links or other content below it. Put your mouse over one of the items above to see what it does. simplepage.noitems_error_user=There are no items on this page currently visible to you. @@ -173,6 +178,7 @@ simplepage.additional-instructions-label=Frequently Asked Questions about multim simplepage.additional-website-instructions-label=Frequently Asked Questions about uploading content from ZIP file simplepage.link=Add Link +simplepage.link.uclabel=LINK simplepage.addlink_header=Add A New Link simplepage.addLink_label=URL: simplepage.addLink_label_add=Or add a URL: @@ -190,6 +196,7 @@ simplepage.name_label=Item Name simplepage.edititem_header=Edit Item simplepage.page_settings=Page settings simplepage.multimedia=Embed +simplepage.multimedia.uclabel=EMBED simplepage.multimedia.tooltip=Add an image, video, Flash file, web page, etc. Use this to embed the item on this page. Use Add Content Link instead if you want a link to an item rather than showing it on the page. simplepage.multimedia-descrip=Add an image, video, Flash file, web page, etc. Use this to embed the item on this page. Use Add Content Link instead if you want a link to an item rather than showing it on the page. simplepage.expert_toggle=Show details @@ -200,6 +207,7 @@ simplepage.tag_object="object" simplepage.tag_img="img" simplepage.tag_iframe="iframe" simplepage.permissions=Permissions +simplepage.permissions.uclabel=PERMISSIONS simplepage.permissions.tooltip=Change permissions associated with this tool for different types of user simplepage.iframe.auto=Use "auto" in height field to use the full height of the content. Only works for URLs within this system. simplepage.nopermissions=You do not have permission to read this page @@ -264,6 +272,7 @@ simplepage.title=Settings simplepage.alt_label=Alt Text simplepage.reorder=Reorder +simplepage.reorder.uclabel=REORDER simplepage.reorder-tooltip=Change order of items, delete items, or import items from another page simplepage.reorder_header=Reorder Page Items simplepage.reorder_instructions=Please drag and drop the items below to reorder them. To delete, drag item to delete bin. @@ -391,6 +400,7 @@ simplepage.comment-you=You simplepage.comment-anonymous-message=Normal users can only see the Anonymous names. The commenters' actual names are shown only to you and other site administrators. simplepage.edit-comment=Edit simplepage.comment-edit-message=You may only edit or delete your comment until it is 30 minutes old. +simplepage.gradingheader=Grading simplepage.comment-grading-message=Type the number of points you wish to assign a particular comment in the "Points" box next to the comment, and then press the Enter key to submit the grade. Any grades that are typed in but not submitted are displayed in red. simplepage.comment-author-owner=Page Owner simplepage.nothing-over-100-percent=You may not have a height or width larger than 100%. diff --git a/lessonbuilder/tool/src/webapp/templates/ShowPage.html b/lessonbuilder/tool/src/webapp/templates/ShowPage.html index 12e35bc648c9..df2f177577dc 100644 --- a/lessonbuilder/tool/src/webapp/templates/ShowPage.html +++ b/lessonbuilder/tool/src/webapp/templates/ShowPage.html @@ -125,16 +125,14 @@

class="portlet title-tools title reset Mrphs-toolTitleNav__link Mrphs-toolTitleNav__link--reset" rsf:id="pagetitlelink">

- -