/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.rendering.internal.macro.script;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.URLStreamHandlerFactory;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.StringTokenizer;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.classloader.ExtendedURLClassLoader;
import org.xwiki.classloader.ExtendedURLStreamHandler;
import org.xwiki.classloader.URIClassLoader;
import org.xwiki.component.annotation.Component;
/**
* Supports the following syntax for JARs attached to wiki pages:
* {@code attach:(wiki):(space).(page)@(attachment)}.
*
* @version $Id: 2df95e73b8ac06265c94b64bd4c01f2a4b88e225 $
* @since 2.0.1
*/
@Component
@Singleton
public class DefaultAttachmentClassLoaderFactory implements AttachmentClassLoaderFactory
{
/**
* The prefix to specify a JAR attached to a wiki page. This is because we also support {@code http://} prefixes
* to load JARs from remote locations.
*/
private static final String ATTACHMENT_PREFIX = "attach:";
/**
* The Stream handler factory to use in the created classloader in order to be able to load our custom
* {@code attachmentjar} custom protocol.
*/
@Inject
private URLStreamHandlerFactory streamHandlerFactory;
/**
* The stream handler for our custom {@code attachmentjar} protocol. We use it to get access to the protocol
* name and to transform from URI to URL.
*/
@Inject
@Named("attachmentjar")
private ExtendedURLStreamHandler attachmentJarHandler;
@Override
public ExtendedURLClassLoader createAttachmentClassLoader(String jarURLs, ClassLoader parent) throws Exception
{
URI[] uris = extractURIs(jarURLs).toArray(new URI[0]);
return new URIClassLoader(uris, parent, this.streamHandlerFactory);
}
@Override
public void extendAttachmentClassLoader(String jarURLs, ExtendedURLClassLoader source) throws Exception
{
for (URI uri : extractURIs(jarURLs)) {
if (uri.getScheme().equalsIgnoreCase(this.attachmentJarHandler.getProtocol())) {
source.addURL(new URL(null, uri.toString(),
this.streamHandlerFactory.createURLStreamHandler(uri.getScheme())));
} else {
source.addURL(uri.toURL());
}
}
}
/**
* @param jarURLs the comma-separated list of JARs locations, specified using either an already registered
* protocol (such as {@code http}) or using the format {@code attach:(wiki):(space).(page)@(filename)}.
* @return the list of URIs
* @throws URISyntaxException in case of an invalid URI
*/
private Set<URI> extractURIs(String jarURLs) throws URISyntaxException
{
// Parse the passed JAR URLs to tokenize it.
Set<URI> uris = new LinkedHashSet<URI>();
if (StringUtils.isNotEmpty(jarURLs)) {
StringTokenizer tokenizer = new StringTokenizer(jarURLs, ",");
while (tokenizer.hasMoreElements()) {
String token = tokenizer.nextToken().trim();
if (token.startsWith(ATTACHMENT_PREFIX)) {
uris.add(createURI(token));
} else {
uris.add(new URI(token));
}
}
}
return uris;
}
/**
* @param attachmentReference the attachment reference in the form {@code attach:(wiki):(space).(page)@(filename)}
* @return a URI of the form {@code attachmentjar://(wiki):(space).(page)@(filename)}.
* The {@code (wiki):(space).(page)@(filename)} part is URL-encoded
* @throws URISyntaxException in case of an invalid URI
*/
private URI createURI(String attachmentReference) throws URISyntaxException
{
String uriBody = attachmentReference.substring(ATTACHMENT_PREFIX.length());
try {
// Note: we encode using UTF8 since it's the W3C recommendation.
// See http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars
// TODO: Once the xwiki-url module is usable, refactor this code to use it and remove the need to
// perform explicit encoding here.
uriBody = URLEncoder.encode(uriBody, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Not supporting UTF-8 as a valid encoding for some reasons. We consider XWiki cannot work
// without that encoding.
throw new RuntimeException("Failed to URL encode [" + uriBody + "] using UTF-8.", e);
}
return new URI(this.attachmentJarHandler.getProtocol() + "://" + uriBody);
}
}