/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache 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.apache.org/licenses/LICENSE-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.apache.wicket.examples.source;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.wicket.Component;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.PopupCloseLink;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IDetachable;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.lang.PackageName;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import com.uwyn.jhighlight.renderer.Renderer;
import com.uwyn.jhighlight.renderer.XhtmlRendererFactory;
/**
* Displays the resources in a packages directory in a browsable format.
*
* @author Martijn Dashorst
*/
public class SourcesPage extends WebPage
{
private static final Log log = LogFactory.getLog(SourcesPage.class);
/**
* Model for retrieving the source code from the classpath of a packaged
* resource.
*/
public class SourceModel extends AbstractReadOnlyModel
{
/**
* Constructor.
*/
public SourceModel()
{
}
/**
* Returns the contents of the file loaded from the classpath.
*
* @return the contents of the file identified by name
*/
public Object getObject()
{
// name contains the name of the selected file
if (Strings.isEmpty(name))
{
return "";
}
BufferedReader br = null;
try
{
StringBuffer sb = new StringBuffer();
InputStream resourceAsStream = page.getResourceAsStream(name);
if (resourceAsStream == null)
{
return "Unable to read the source for " + name;
}
br = new BufferedReader(new InputStreamReader(resourceAsStream));
while (br.ready())
{
sb.append(br.readLine());
sb.append("\n");
}
int lastDot = name.lastIndexOf('.');
if (lastDot != -1)
{
String type = name.substring(lastDot + 1);
Renderer renderer = XhtmlRendererFactory.getRenderer(type);
if (renderer != null)
{
return renderer.highlight(name, sb.toString(), "UTF-8", true);
}
}
return Strings.escapeMarkup(sb.toString(), false, true).toString().replaceAll("\n", "<br />");
}
catch (IOException e)
{
log.error("Unable to read resource stream for: " + name + "; Page="
+ page.toString(), e);
return "";
}
finally
{
IOUtils.closeQuietly(br);
}
}
}
/**
* Model for retrieving the contents of a package directory from the class
* path.
*/
public class PackagedResourcesModel extends AbstractReadOnlyModel implements IDetachable
{
private final List resources = new ArrayList();
/**
* Constructor.
*/
public PackagedResourcesModel()
{
}
/**
* Clears the list to save space.
*/
protected void onDetach()
{
resources.clear();
}
/**
* Returns the list of resources found in the package of the page.
*
* @return the list of resources found in the package of the page.
*/
public Object getObject()
{
if (resources.isEmpty())
{
get(page);
// PackageName name = PackageName.forClass(page);
// ClassLoader loader = page.getClassLoader();
// String path = Strings.replaceAll(name.getName(), ".", "/").toString();
// try
// {
// // gives the urls for each place where the package
// // path could be found. There could be multiple
// // jar files containing the same package, so each
// // jar file has its own url.
//
// Enumeration urls = loader.getResources(path);
// while (urls.hasMoreElements())
// {
// URL url = (URL)urls.nextElement();
//
// // the url points to the directory structure
// // embedded in the classpath.
//
// getPackageContents(url);
// }
// }
// catch (IOException e)
// {
// log.error("Unable to read resource for: " + path, e);
// }
}
return resources;
}
/**
* Retrieves the package contents for the given URL.
*
* @param packageListing
* the url to list.
*/
private void getPackageContents(URL packageListing)
{
BufferedReader br = null;
try
{
InputStream openStream = packageListing.openStream();
if (openStream == null)
{
return;
}
br = new BufferedReader(new InputStreamReader(openStream));
while (br.ready())
{
String listing = br.readLine();
String extension = Strings.afterLast(listing, '.');
if (!listing.endsWith("class"))
{
resources.add(listing);
}
}
}
catch (IOException e)
{
log.error("Unable to get package content: " + packageListing.toString(), e);
}
finally
{
IOUtils.closeQuietly(br);
}
}
private final void addResources(final Class scope, final AppendingStringBuffer relativePath, final File dir)
{
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++)
{
File file = files[i];
if (file.isDirectory())
{
addResources(scope, new AppendingStringBuffer(relativePath).append(file.getName()).append('/'), file);
}
else
{
String name = file.getName();
String extension = Strings.afterLast(name, '.');
if (!name.endsWith("class"))
{
resources.add(relativePath + name);
}
}
}
}
private void get(Class scope)
{
String packageRef = Strings.replaceAll(PackageName.forClass(scope).getName(), ".", "/").toString();
ClassLoader loader = scope.getClassLoader();
try
{
// loop through the resources of the package
Enumeration packageResources = loader.getResources(packageRef);
while (packageResources.hasMoreElements())
{
URL resource = (URL)packageResources.nextElement();
URLConnection connection = resource.openConnection();
if (connection instanceof JarURLConnection)
{
JarFile jf = ((JarURLConnection)connection).getJarFile();
scanJarFile(scope, packageRef, jf);
}
else
{
String absolutePath = scope.getResource("").toExternalForm();
File basedir;
URI uri;
try
{
uri = new URI(absolutePath);
}
catch (URISyntaxException e)
{
throw new RuntimeException(e);
}
try
{
basedir = new File(uri);
}
catch(IllegalArgumentException e)
{
log.debug("Can't construct the uri as a file: " + absolutePath);
// if this is throwen then the path is not really a file. but could be a zip.
String jarZipPart = uri.getSchemeSpecificPart();
// lowercased for testing if jar/zip, but leave the real filespec unchanged
String lowerJarZipPart = jarZipPart.toLowerCase();
int index = lowerJarZipPart.indexOf(".zip");
if(index == -1) index = lowerJarZipPart.indexOf(".jar");
if(index == -1) throw e;
String filename = jarZipPart.substring(0, index+4); // 4 = len of ".jar" or ".zip"
log.debug("trying the filename: " + filename + " to load as a zip/jar.");
JarFile jarFile = new JarFile(filename,false);
scanJarFile(scope, packageRef, jarFile);
return;
}
if (!basedir.isDirectory())
{
throw new IllegalStateException("unable to read resources from directory "
+ basedir);
}
addResources(scope, new AppendingStringBuffer(), basedir);
}
}
}
catch (IOException e)
{
throw new WicketRuntimeException(e);
}
Collections.sort(resources);
return;
}
private void scanJarFile(Class scope,String packageRef, JarFile jf)
{
Enumeration enumeration = jf.entries();
while (enumeration.hasMoreElements())
{
JarEntry je = (JarEntry)enumeration.nextElement();
String name = je.getName();
if (name.startsWith(packageRef))
{
name = name.substring(packageRef.length() + 1);
String extension = Strings.afterLast(name, '.');
if (!name.endsWith("class"))
{
resources.add(name);
}
}
}
}
}
/**
* Displays the resources embedded in a package in a list.
*/
public class FilesBrowser extends WebMarkupContainer
{
/**
* Constructor.
*
* @param id
* the component identifier
*/
public FilesBrowser(String id)
{
super(id);
ListView lv = new ListView("file", new PackagedResourcesModel())
{
protected void populateItem(ListItem item)
{
AjaxFallbackLink link = new AjaxFallbackLink("link", item.getModel())
{
public void onClick(AjaxRequestTarget target)
{
setName(getModelObjectAsString());
if (target != null)
{
target.addComponent(codePanel);
target.addComponent(filename);
}
}
};
link.add(new Label("name", item.getModelObjectAsString()));
item.add(link);
}
};
add(lv);
}
}
/**
* Container for displaying the source of the selected page, resource or
* other element from the package.
*/
public class CodePanel extends WebMarkupContainer
{
/**
* Constructor.
*
* @param id
* the component id
*/
public CodePanel(String id)
{
super(id);
Label code = new Label("code", new SourceModel());
code.setEscapeModelStrings(false);
code.setOutputMarkupId(true);
add(code);
}
}
/**
* The selected name of the packaged resource to display.
*/
private String name;
/**
* The class of the page of which the sources need to be displayed.
*/
private Class page;
/**
* The panel for setting the ajax calls.
*/
private Component codePanel;
private Label filename;
/**
* Sets the name.
*
* @param name
* the name to set.
*/
public void setName(String name)
{
this.name = name;
}
/**
* Gets the name.
*
* @return the name.
*/
public String getName()
{
return name;
}
/**
* Default constructor, only used for test purposes.
*/
public SourcesPage()
{
this(SourcesPage.class);
}
/**
* Constructor.
*
* @param page
* the page where the sources need to be shown from.
*/
public SourcesPage(Class page)
{
this.page = page;
filename = new Label("filename", new PropertyModel(this, "name"));
filename.setOutputMarkupId(true);
add(filename);
codePanel = new CodePanel("codepanel").setOutputMarkupId(true);
add(codePanel);
add(new FilesBrowser("filespanel"));
add(new PopupCloseLink("close"));
}
}