Skip to content

Commit 7d53cd3

Browse files
authored
KNL-1284 Allow paths inside WEB-INF for tool registration (sakaiproject#2174)
* KNL-1284 Allow paths inside WEB-INF for tool registration * KNL-1284 tool reg from /tools/ /WEB-INF/tools/ We allow registrations from multiple locations so that contrib tools continue to work without changes but all the core tools can move their files to a better location. * KNL-1284 Move tool registration files to WEB-INF This means that the tool registration files are no longer accessible through requests to the webapps directly. This change was done with: find . -type d -path '*/webapp/tools' | while read dir; do pushd $dir; cd ..; if [ -d WEB-INF ]; then git mv tools WEB-INF; fi; popd; done
1 parent 9e34c40 commit 7d53cd3

File tree

91 files changed

+289
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+289
-42
lines changed

kernel/api/src/main/java/org/sakaiproject/util/ToolListener.java

+117-42
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,25 @@
2121

2222
package org.sakaiproject.util;
2323

24-
import java.io.File;
25-
import java.util.Iterator;
26-
import java.util.Set;
24+
import org.sakaiproject.component.api.ServerConfigurationService;
25+
import org.sakaiproject.component.cover.ComponentManager;
26+
import org.sakaiproject.tool.api.ActiveToolManager;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2729

30+
import javax.servlet.ServletContext;
2831
import javax.servlet.ServletContextEvent;
2932
import javax.servlet.ServletContextListener;
30-
31-
import org.sakaiproject.component.cover.ServerConfigurationService;
32-
import org.sakaiproject.tool.cover.ActiveToolManager;
33-
import org.slf4j.Logger;
34-
import org.slf4j.LoggerFactory;
33+
import java.io.File;
34+
import java.util.*;
35+
import java.util.stream.Collectors;
3536

3637
/**
3738
* <p>
3839
* Webapp listener to detect webapp-housed tool registration.<br/>
3940
* SAK-8908:<br/>
4041
* Re-wrote the contextInitialized() method to add tool localization files to the
41-
* newly registered tool(s). These files are required to be in the /tool/ directory
42+
* newly registered tool(s). These files are required to be in the tools directory
4243
* have have a name of the form [toolId][_][localCode].properties. This format allows
4344
* tool litles (etc) to be localized even when multiple tool registrations are included.
4445
* </p>
@@ -50,42 +51,73 @@
5051
* &lt;/listener&gt;<br/>
5152
* </code>
5253
* </p>
54+
* <p>
55+
* By default the tools directories looked at in the webapp are /tools/ and /WEB-INF/tools/ . It is
56+
* recommended that all tool registration files are put in /WEB-INF/tools/ as the files don't need to
57+
* served up by the container, however /tools/ is supported for backwards compatibility. If you wish to
58+
* use a custom location set an parameter on the tool listener:
59+
* </p>
60+
* <p>
61+
* <code>
62+
* &lt;context-param&gt;<br>
63+
* &nbsp;&lt;param-name&gt;org.sakaiproject.util.ToolListener.PATH&lt;/param-name&gt;<br>
64+
* &nbsp;&lt;param-value&gt;/mypath/&lt;/param-value&gt;<br>
65+
* &lt;/context-param&gt;<br>
66+
* </code>
67+
* </p>
5368
*/
5469
public class ToolListener implements ServletContextListener
5570
{
5671
private static final Logger M_log = LoggerFactory.getLogger(ToolListener.class);
5772

73+
/**
74+
* The content parameter in your web.xml specifying the webapp root relative
75+
* path to look in for the tool registration files.
76+
*/
77+
public final static String PATH = ToolListener.class.getName()+".PATH";
78+
79+
private final ActiveToolManager activeToolManager;
80+
private final ServerConfigurationService serverConfigurationService;
81+
82+
public ToolListener()
83+
{
84+
activeToolManager = ComponentManager.get(ActiveToolManager.class);
85+
serverConfigurationService = ComponentManager.get(ServerConfigurationService.class);
86+
}
87+
88+
public ToolListener(ActiveToolManager activeToolManager, ServerConfigurationService serverConfigurationService)
89+
{
90+
this.activeToolManager = activeToolManager;
91+
this.serverConfigurationService = serverConfigurationService;
92+
}
93+
5894
/**
5995
* Initialize.
6096
*/
6197
public void contextInitialized(ServletContextEvent event)
6298
{
99+
final String sakaiHomePath = serverConfigurationService.getSakaiHomePath();
63100
// The the location of resource and registration files.
64-
Set paths = event.getServletContext().getResourcePaths("/tools/");
65-
final String sakaiHomePath = ServerConfigurationService.getSakaiHomePath();
66-
67-
// First Pass: Search for tool registration file.
68-
if (paths == null) {
69-
return;
70-
}
101+
ServletContext context = event.getServletContext();
102+
Set<String> paths = getToolsPaths(context);
103+
if (paths == null) return;
71104
int registered = 0;
72-
for (Iterator i = paths.iterator(); i.hasNext();)
73-
{
74-
final String path = (String) i.next();
75-
105+
// First Pass: Search for tool registration files
106+
for (final String path : paths) {
76107
// skip directories
77108
if (path.endsWith("/")) continue;
78109

79110
// If an XML file, use it as the tool registration file.
80-
if (path.endsWith(".xml"))
81-
{
82-
final File f = new File(sakaiHomePath + path);
83-
if(f.exists()) {
84-
ActiveToolManager.register(f, event.getServletContext());
111+
if (path.endsWith(".xml")) {
112+
String file = path.substring(path.lastIndexOf("/") + 1);
113+
// overrides are always in a folder called /tools/
114+
final File f = new File(sakaiHomePath + "/tools/" + file);
115+
if (f.exists()) {
116+
activeToolManager.register(f, context);
85117
M_log.info("overriding tools configuration: registering tools from resource: " + sakaiHomePath + path);
86118
} else {
87119
M_log.info("registering tools from resource: " + path);
88-
ActiveToolManager.register(event.getServletContext().getResourceAsStream(path), event.getServletContext());
120+
activeToolManager.register(context.getResourceAsStream(path), context);
89121
}
90122
registered++;
91123
}
@@ -98,39 +130,82 @@ public void contextInitialized(ServletContextEvent event)
98130
}
99131

100132
// Second pass, search for message bundles. Two passes are necessary to make sure the tool is registered first.
101-
for (Iterator j = paths.iterator(); j.hasNext();)
102-
{
103-
String path = (String) j.next();
104-
133+
for (String path : paths) {
105134
// skip directories
106135
if (path.endsWith("/")) continue;
107136

108137
// Check for a message properties file.
109-
if (path.endsWith(".properties"))
110-
{
138+
if (path.endsWith(".properties")) {
111139
// Extract the tool id from the resource file name.
112-
File reg = new File (path);
140+
File reg = new File(path);
113141
String tn = reg.getName();
114-
String tid = null;
142+
String tid;
115143
if (tn.indexOf('_') == -1)
116-
tid = tn.substring (0, tn.lastIndexOf('.')); // Default file.
144+
tid = tn.substring(0, tn.lastIndexOf('.')); // Default file.
117145
else
118-
tid = tn.substring (0, tn.indexOf('_')); // Locale-based file.
146+
tid = tn.substring(0, tn.indexOf('_')); // Locale-based file.
119147

120-
String msg = event.getServletContext().getRealPath(path.substring (0, path.lastIndexOf('.'))+".properties");
121-
if (tid != null)
122-
{
123-
ActiveToolManager.setResourceBundle (tid, msg);
124-
M_log.info("Added localization resources for " + tid);
125-
}
148+
String msg = context.getRealPath(path.substring(0, path.lastIndexOf('.')) + ".properties");
149+
activeToolManager.setResourceBundle(tid, msg);
150+
M_log.info("Added localization " + tn + "resources for " + tid);
126151
}
127152
}
128153
}
129154

155+
/**
156+
* This looks for the tools folders that shoudl be used.
157+
* @param context The ServletContext
158+
* @return A list of all possible tool registration files or <code>null</code> if the containing folders weren't found.
159+
*/
160+
private Set<String> getToolsPaths(ServletContext context) {
161+
Collection<String> toolFolders = getToolsFolders(context);
162+
Set<String> paths = new HashSet<>();
163+
toolFolders.stream().map(context::getResourcePaths).filter(files -> files != null).forEach(paths::addAll);
164+
if (paths.isEmpty())
165+
{
166+
// Warn if the listener is setup but no tools found.
167+
M_log.warn("No tools folder found: "+
168+
toolFolders.stream().map(context::getRealPath).collect(Collectors.joining(", ")));
169+
return null;
170+
}
171+
return paths;
172+
}
173+
130174
/**
131175
* Destroy.
132176
*/
133177
public void contextDestroyed(ServletContextEvent event)
134178
{
135179
}
180+
181+
/**
182+
* This locates the tool registration folder inside the webapp.
183+
* @param context The servlet context.
184+
* @return The standard tool registration folder location or the configured value.
185+
*/
186+
protected Collection<String> getToolsFolders(ServletContext context)
187+
{
188+
String path = context.getInitParameter(PATH);
189+
Collection<String> paths;
190+
if (path == null)
191+
{
192+
Collection<String> defaultPaths = new LinkedList<>();
193+
defaultPaths.add("/WEB-INF/tools/");
194+
defaultPaths.add("/tools/");
195+
paths = defaultPaths;
196+
}
197+
else
198+
{
199+
if (!path.startsWith("/"))
200+
{
201+
path = "/"+ path;
202+
}
203+
if (!path.endsWith("/"))
204+
{
205+
path = path+ "/";
206+
}
207+
paths = Collections.singleton(path);
208+
}
209+
return paths;
210+
}
136211
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/* *********************************************************************************
2+
* $URL$
3+
* $Id$
4+
* *********************************************************************************
5+
*
6+
* Copyright (c) 2016 Sakai Foundation
7+
*
8+
* Licensed under the Educational Community License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.opensource.org/licenses/ECL-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*
20+
* ********************************************************************************/
21+
package org.sakaiproject.util;
22+
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.mockito.Mock;
27+
import org.mockito.runners.MockitoJUnitRunner;
28+
import org.sakaiproject.component.api.ServerConfigurationService;
29+
import org.sakaiproject.tool.api.ActiveToolManager;
30+
31+
import javax.servlet.ServletContext;
32+
import javax.servlet.ServletContextEvent;
33+
import java.io.IOException;
34+
import java.io.InputStream;
35+
import java.nio.file.Files;
36+
import java.nio.file.Path;
37+
import java.util.stream.Collectors;
38+
import java.util.stream.Stream;
39+
40+
import static org.mockito.Mockito.*;
41+
42+
/**
43+
* Tests the basic functions of the tool listener
44+
*/
45+
@RunWith(MockitoJUnitRunner.class)
46+
public class ToolListenerTest {
47+
48+
private ToolListener listener;
49+
50+
@Mock
51+
private ServletContextEvent event;
52+
53+
@Mock
54+
private ServletContext context;
55+
56+
@Mock
57+
private ServerConfigurationService serverConfigurationService;
58+
59+
@Mock
60+
private ActiveToolManager activeToolManager;
61+
62+
private Path sakaiHome;
63+
private Path toolsFolder;
64+
65+
@Before
66+
public void setUp() throws IOException {
67+
listener = new ToolListener(activeToolManager, serverConfigurationService);
68+
when(event.getServletContext()).thenReturn(context);
69+
// Create the tools folder inside our pretend sakai-home
70+
sakaiHome = Files.createTempDirectory("ToolListenerTest");
71+
toolsFolder = Files.createDirectories(sakaiHome.resolve("tools"));
72+
when(serverConfigurationService.getSakaiHomePath()).thenReturn(sakaiHome.toString());
73+
doAnswer(invocation -> "/webapp"+ invocation.getArgumentAt(0, String.class))
74+
.when(context).getRealPath(anyString());
75+
}
76+
77+
// Testing that it loads inside the /tools folder
78+
@Test
79+
public void testToolsRegistration() {
80+
when(context.getResourcePaths("/tools/")).thenReturn(Stream.of("/tools/", "/tools/sakai-tool.xml").collect(Collectors.toSet()));
81+
InputStream inputStream = mock(InputStream.class);
82+
when(context.getResourceAsStream("/tools/sakai-tool.xml")).thenReturn(inputStream);
83+
listener.contextInitialized(event);
84+
verify(activeToolManager).register(inputStream, context);
85+
}
86+
87+
// Testing that it loads inside the /WEB-INF/tools folder
88+
@Test
89+
public void testWebInfRegistration() {
90+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/", "/WEB-INF/tools/sakai-tool.xml").collect(Collectors.toSet()));
91+
InputStream inputStream = mock(InputStream.class);
92+
when(context.getResourceAsStream("/WEB-INF/tools/sakai-tool.xml")).thenReturn(inputStream);
93+
listener.contextInitialized(event);
94+
verify(activeToolManager).register(inputStream, context);
95+
}
96+
97+
// Testing is doesn't fall over when no registrations found
98+
@Test
99+
public void testNoRegistration() {
100+
when(context.getResourcePaths("/tools/")).thenReturn(Stream.of("/tools/" ).collect(Collectors.toSet()));
101+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/" ).collect(Collectors.toSet()));
102+
InputStream inputStream = mock(InputStream.class);
103+
listener.contextInitialized(event);
104+
verify(activeToolManager, never()).register(inputStream, context);
105+
}
106+
107+
// Check multiple registrations work across multiple locations.
108+
@Test
109+
public void testMultipleRegistrations() {
110+
when(context.getResourcePaths("/tools/")).thenReturn(Stream.of("/tools/", "/tools/sakai-tool.xml").collect(Collectors.toSet()));
111+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/", "/WEB-INF/tools/sakai-tool.xml", "/WEB-INF/tools/another-tool.xml").collect(Collectors.toSet()));
112+
InputStream inputStream = mock(InputStream.class);
113+
when(context.getResourceAsStream(anyString())).thenReturn(inputStream);
114+
listener.contextInitialized(event);
115+
verify(activeToolManager, times(3)).register(inputStream, context);
116+
}
117+
118+
// Check the folder and bad extension are ignored.
119+
@Test
120+
public void testIgnoreFiles() {
121+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/", "/WEB-INF/tools/sakai-tool.ignored", "/WEB-INF/tools/folder/").collect(Collectors.toSet()));
122+
InputStream inputStream = mock(InputStream.class);
123+
when(context.getResourceAsStream(anyString())).thenReturn(inputStream);
124+
listener.contextInitialized(event);
125+
verify(activeToolManager, never()).register(inputStream, context);
126+
}
127+
128+
// Check that a custom location is set it doesn't use the standard ones.
129+
@Test
130+
public void testCustomLocation() {
131+
when(context.getInitParameter(ToolListener.PATH)).thenReturn("/custom");
132+
when(context.getResourcePaths("/custom/")).thenReturn(Stream.of("/custom/", "/custom/sakai-tool.xml").collect(Collectors.toSet()));
133+
InputStream inputStream = mock(InputStream.class);
134+
when(context.getResourceAsStream("/custom/sakai-tool.xml")).thenReturn(inputStream);
135+
listener.contextInitialized(event);
136+
verify(activeToolManager).register(inputStream, context);
137+
verify(context, never()).getResourcePaths("/tools/");
138+
verify(context, never()).getResourcePaths("/WEB-INF/tools/");
139+
}
140+
141+
// Check that a tool override is loaded from sakai.home ok.
142+
@Test
143+
public void testToolOverride() throws IOException {
144+
Path sakaiTool = Files.createFile(toolsFolder.resolve("sakai-tool.xml"));
145+
Path otherTool = Files.createFile(toolsFolder.resolve("other-tool.xml"));
146+
when(context.getResourcePaths("/tools/")).thenReturn(Stream.of("/tools/", "/tools/sakai-tool.xml").collect(Collectors.toSet()));
147+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/", "/WEB-INF/tools/other-tool.xml").collect(Collectors.toSet()));
148+
listener.contextInitialized(event);
149+
verify(activeToolManager, never()).register(any(InputStream.class), eq(context));
150+
verify(activeToolManager).register(sakaiTool.toFile(), context);
151+
verify(activeToolManager).register(otherTool.toFile(), context);
152+
}
153+
154+
// Check that locale files are loaded out of the old tools folder.
155+
@Test
156+
public void testMessageBundle() {
157+
when(context.getResourcePaths("/tools/")).thenReturn(Stream.of("/tools/", "/tools/sakai-tool.xml", "/tools/sakai-tool.properties", "/tools/sakai-tool_fr.properties").collect(Collectors.toSet()));
158+
listener.contextInitialized(event);
159+
verify(activeToolManager).setResourceBundle("sakai-tool", "/webapp/tools/sakai-tool.properties");
160+
verify(activeToolManager).setResourceBundle("sakai-tool", "/webapp/tools/sakai-tool_fr.properties");
161+
}
162+
163+
// Check that locale files are loaded out of the new tools folder.
164+
@Test
165+
public void testMessageBundleWebInf() {
166+
when(context.getResourcePaths("/WEB-INF/tools/")).thenReturn(Stream.of("/WEB-INF/tools/", "/WEB-INF/tools/sakai-tool.xml", "/WEB-INF/tools/sakai-tool.properties", "/WEB-INF/tools/sakai-tool_fr.properties").collect(Collectors.toSet()));
167+
listener.contextInitialized(event);
168+
verify(activeToolManager).setResourceBundle("sakai-tool", "/webapp/WEB-INF/tools/sakai-tool.properties");
169+
verify(activeToolManager).setResourceBundle("sakai-tool", "/webapp/WEB-INF/tools/sakai-tool_fr.properties");
170+
}
171+
172+
}

0 commit comments

Comments
 (0)