/* * 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.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; 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.Optional; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxCallListener; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.authorization.UnauthorizedInstantiationException; import org.apache.wicket.core.util.lang.WicketObjects; 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.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.request.http.handler.ErrorCodeRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; 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.StringValue; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 Logger log = LoggerFactory.getLogger(SourcesPage.class); /** * Model for retrieving the source code from the classpath of a packaged resource. */ private class SourceModel implements IModel<String> { /** * Returns the contents of the file loaded from the classpath. * * @return the contents of the file identified by name */ @Override public String getObject() { // name contains the name of the selected file StringValue sourceParam = getPageParameters().get(SOURCE); if (Strings.isEmpty(name) && sourceParam.isEmpty()) { return ""; } String source = null; InputStream resourceAsStream = null; try { source = (name != null) ? name : sourceParam.toString(); resourceAsStream = getPageTargetClass().getResourceAsStream(source); if (resourceAsStream == null) { return "Unable to read the source for " + source; } int lastDot = source.lastIndexOf('.'); if (lastDot != -1) { String type = source.substring(lastDot + 1); Renderer renderer = XhtmlRendererFactory.getRenderer(type); if (renderer != null) { ByteArrayOutputStream output = new ByteArrayOutputStream(); renderer.highlight(source, resourceAsStream, output, "UTF-8", true); return output.toString("UTF-8"); } } CharSequence escaped = Strings.escapeMarkup(IOUtils.toString(resourceAsStream), false, true); return Strings.replaceAll(escaped, "\n", "<br />").toString(); } catch (IOException e) { log.error( "Unable to read resource stream for: " + source + "; Page=" + page.toString(), e); return ""; } finally { IOUtils.closeQuietly(resourceAsStream); } } } /** * Model for retrieving the contents of a package directory from the class path. */ private class PackagedResourcesModel extends LoadableDetachableModel<List<String>> { /** * Returns the list of resources found in the package of the page. * * @return the list of resources found in the package of the page. */ @Override protected List<String> load() { return get(getPageTargetClass()); } private void addResources(final AppendingStringBuffer relativePath, final File dir, List<String> resources) { File[] files = dir.listFiles(); for (File file : files) { if (file.isDirectory()) { addResources(new AppendingStringBuffer(relativePath).append(file.getName()) .append('/'), file, resources); } else { String name = file.getName(); if (!name.endsWith("class")) { resources.add(relativePath + name); } } } } private List<String> get(Class<?> scope) { List<String> resources = new ArrayList<>(); String packageRef = Strings.replaceAll(PackageName.forClass(scope).getName(), ".", "/") .toString(); ClassLoader loader = scope.getClassLoader(); try { // loop through the resources of the package Enumeration<URL> packageResources = loader.getResources(packageRef); while (packageResources.hasMoreElements()) { URL resource = packageResources.nextElement(); URLConnection connection = resource.openConnection(); if (connection instanceof JarURLConnection) { JarFile jf = ((JarURLConnection)connection).getJarFile(); scanJarFile(packageRef, jf, resources); } 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(packageRef, jarFile, resources); return resources; } if (!basedir.isDirectory()) { throw new IllegalStateException( "unable to read resources from directory " + basedir); } addResources(new AppendingStringBuffer(), basedir, resources); } } } catch (IOException e) { throw new WicketRuntimeException(e); } Collections.sort(resources); return resources; } private void scanJarFile(String packageRef, JarFile jf, List<String> resources) { Enumeration<JarEntry> enumeration = jf.entries(); while (enumeration.hasMoreElements()) { JarEntry je = enumeration.nextElement(); String name = je.getName(); if (name.startsWith(packageRef)) { name = name.substring(packageRef.length() + 1); if (!name.endsWith("class")) { resources.add(name); } } } } } /** * Displays the resources embedded in a package in a list. */ private class FilesBrowser extends WebMarkupContainer { /** * Constructor. * * @param id * the component identifier */ private FilesBrowser(String id) { super(id); ListView<String> lv = new ListView<String>("file", new PackagedResourcesModel()) { @Override protected void populateItem(final ListItem<String> item) { AjaxFallbackLink<String> link = new AjaxFallbackLink<String>("link", item.getModel()) { @Override public void onClick(Optional<AjaxRequestTarget> targetOptional) { setName(getDefaultModelObjectAsString()); targetOptional.ifPresent(target -> { target.add(codePanel); target.add(filename); }); } @Override protected CharSequence getURL() { return urlFor(SourcesPage.class, SourcesPage.generatePageParameters( getPageTargetClass(), item.getModel().getObject())); } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); AjaxCallListener ajaxCallListener = new AjaxCallListener() { @Override public CharSequence getFailureHandler(Component component) { return "window.location=this.href;"; } }; attributes.getAjaxCallListeners().add(ajaxCallListener); attributes.getExtraParameters().put(PAGE_CLASS, "1"); } }; link.add(new Label("name", item.getDefaultModelObjectAsString())); item.add(link); } }; add(lv); } } /** * Container for displaying the source of the selected page, resource or other element from the * package. */ private class CodePanel extends WebMarkupContainer { /** * Constructor. * * @param id * the component id */ private CodePanel(String id) { super(id); Label code = new Label("code", new SourceModel()); code.setEscapeModelStrings(false); code.setOutputMarkupId(true); add(code); } } /** * Parameter key for identifying the page class. UUID generated. */ public static final String PAGE_CLASS = SourcesPage.class.getSimpleName() + "_class"; /** * Parameter key for identify the name of the source file in the package. */ public static final String SOURCE = "source"; /** * The selected name of the packaged resource to display. */ private String name; private transient Class<? extends Page> page; /** * The panel for setting the ajax calls. */ private final Component codePanel; private final 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; } /** * * Construct. * * @param params */ public SourcesPage(final PageParameters params) { super(params); filename = new Label("filename", () -> name != null ? name : params.get(SOURCE).toOptionalString()); filename.setOutputMarkupId(true); add(filename); codePanel = new CodePanel("codepanel").setOutputMarkupId(true); add(codePanel); add(new FilesBrowser("filespanel")); } /** * * @param page * @return PageParameters for reconstructing the bookmarkable page. */ public static PageParameters generatePageParameters(Page page) { return generatePageParameters(page.getClass(), null); } /** * * @param clazz * @param fileName * @return PageParameters for reconstructing the bookmarkable page. */ public static PageParameters generatePageParameters(Class<? extends Page> clazz, String fileName) { PageParameters p = new PageParameters(); p.set(PAGE_CLASS, clazz.getName()); if (fileName != null) { p.set(SOURCE, fileName); } return p; } private Class<? extends Page> getPageTargetClass() { if (page == null) { String pageParam = getPageParameters().get(PAGE_CLASS).toOptionalString(); if (pageParam == null) { log.error("key: {} is null.", PAGE_CLASS); getRequestCycle().replaceAllRequestHandlers( new ErrorCodeRequestHandler(404, "Could not find sources for the page you requested")); } else if (!pageParam.startsWith("org.apache.wicket.examples")) { log.error("user is trying to access class: {} which is not in the scope of org.apache.wicket.examples", pageParam); throw new UnauthorizedInstantiationException(getClass()); } page = WicketObjects.resolveClass(pageParam); if (page == null) { getRequestCycle().replaceAllRequestHandlers( new ErrorCodeRequestHandler(404, "Could not find sources for the page you requested")); } } return page; } }