Tapestry source code viewer

A simple implementation of a SourceCodeViewer would be to have a service for adding and listing the source code and a component to display it.

public interface SourceCodeService {

    /**
     * This method contributes a source code resource.
     * 
     * @param sourceCodeContribution
     */
    void add(SourceCodeResource sourceCodeContribution);

    /**
     * This method contributes a source code resource.
     * 
     * @param type
     * @param resource
     */
    void add(String type, Resource resource);

    /**
     * This method contributes an optional source code resource.
     * 
     * @param type
     * @param resource
     */
    void addOptional(String type, Resource resource);

    /**
     * This method adds a java source code for a given class.
     * 
     * @param clazz
     */
    void add(Class<?> clazz);

    /**
     * This method contributes a java and template file
     * 
     * @param clazz
     */
    void addComponent(Class<?> clazz);

    /**
     * This method adds a java and template file for a
     * component(page/component/mixin) with the given name.
     * 
     * @param componentName
     */
    void addComponent(String componentName);

    /**
     * @return all source code resources.
     */
    Set<SourceCodeResource> getSourceCodeResources();
}

//Implementation
public class SourceCodeServiceImpl implements SourceCodeService {

    private final Set<SourceCodeResource> resources = new HashSet<SourceCodeResource>();

    private final AssetSource assetSource;

    public SourceCodeServiceImpl(final AssetSource assetSource) {

        this.assetSource = assetSource;
    }

    @Override
    public void add(final SourceCodeResource sourceCodeContribution) {

        this.resources.add(sourceCodeContribution);
    }

    @Override
    public void add(final String type, final Resource resource) {

        add(new SourceCodeResource(type, resource));
    }

    @Override
    public Set<SourceCodeResource> getSourceCodeResources() {

        return this.resources;
    }

    @Override
    public void add(final Class<?> clazz) {

        add(SourceCodeResource.JAVA, getResourcePathFromClass(clazz, "java"));
    }

    @Override
    public void addComponent(final Class<?> clazz) {

        add(clazz);
        add(SourceCodeResource.TML, getResourcePathFromClass(clazz, "tml"));
        addOptional(SourceCodeResource.PROPERTIES,
                getResourcePathFromClass(clazz, "properties"));
    }

    @Override
    public void addComponent(final String componentClass) {

        add(SourceCodeResource.JAVA,
                getResourcePathFromClassName(componentClass, "java"));
        addOptional(SourceCodeResource.TML,
                getResourcePathFromClassName(componentClass, "tml"));
        addOptional(SourceCodeResource.PROPERTIES,
                getResourcePathFromClassName(componentClass, "properties"));
    }

    private Resource getResourcePathFromClassName(final String componentClass,
            final String extension) {

        return this.assetSource.resourceForPath("classpath:/"
                + componentClass.replace('.', '/') + "." + extension);
    }

    private Resource getResourcePathFromClass(final Class<?> clazz,
            final String extension) {

        return this.assetSource.resourceForPath("classpath:/"
                + clazz.getCanonicalName().replace('.', '/') + "." + extension);
    }

    @Override
    public void addOptional(final String type, final Resource resource) {

        add(new SourceCodeResource(type, resource, true));
    }

}

public class SourceCodeResource {

    /** CSS resource type */
    public static final String CSS = "css";

    /** JavaScript resource type */
    public static final String JS = "js";

    /** Java resource type */
    public static final String JAVA = "java";

    /** Tapestry Template resource type */
    public static final String TML = "xml";

    /** Properties file resource type */
    public static final String PROPERTIES = "properties";

    private final String type;

    private final Resource resource;

    private final boolean optional;

    public SourceCodeResource(final String type, final Resource resource,
            final boolean optional) {

        this.type = type;
        this.resource = resource;
        this.optional = optional;
    }

    public SourceCodeResource(final String type, final Resource resource) {

        this(type, resource, false);
    }

    public String getType() {

        return this.type;
    }

    public Resource getResource() {

        return this.resource;
    }

    public boolean getOptional() {

        return this.optional;
    }

    @Override
    public int hashCode() {

        return this.resource.hashCode();
    }

    @Override
    public boolean equals(final Object other) {

        if (other == this) {
            return true;
        }

        if (other == null || !(other instanceof SourceCodeResource)) {
            return false;
        }

        final SourceCodeResource otherResource = (SourceCodeResource) other;

        return this.resource.equals(otherResource.getResource());
    }
}

SourceCodeViewer assumes that the source code has been copied to the build/target directory.

public class SourceCodeViewer {

    /** Class name. */
    private static final String CLASS_NAME = SourceCodeViewer.class
            .getSimpleName();

    @Inject
    private SourceCodeService sourceCodeService;

    @Property
    private SourceCodeResource sourceCodeResource;

    @Property
    private List<SourceCodeResource> sourceCodeResources;

    @SuppressWarnings("unused")
    @Property
    private int index;

    @SetupRender
    void setupSourceCodeResources() {

        this.sourceCodeResources = new ArrayList<SourceCodeResource>(
                this.sourceCodeService.getSourceCodeResources());
        Collections.sort(this.sourceCodeResources,
                new Comparator<SourceCodeResource>() {

                    @Override
                    public int compare(final SourceCodeResource o1,
                            final SourceCodeResource o2) {

                        final int result = o1.getType().toLowerCase()
                                .compareTo(o2.getType().toLowerCase());

                        if (result != 0) {
                            return result;
                        }

                        return o1
                                .getResource()
                                .getFile()
                                .toLowerCase()
                                .compareTo(
                                        o2.getResource().getFile()
                                                .toLowerCase());
                    }

                });
    }

    /**
     * @return resource exists
     */
    public boolean getResourceExists(){
        return !this.sourceCodeResource.getOptional() || this.sourceCodeResource.getResource().exists();
    }

    /**
     * @return String
     */
    public String getSource() {

        // Declare/Initialize
        final ByteArrayOutputStream outputStream;

        try {

            final Resource resource = this.sourceCodeResource.getResource();

            outputStream = new ByteArrayOutputStream();

            TapestryInternalUtils.copy(resource.openStream(), outputStream);

        } catch (final Exception e) {

            throw new RuntimeException(
                    "The source code [" + this.sourceCodeResource.getResource()
                            + "] is not available.", e);
        }

        return new String(outputStream.toByteArray());
    }
}

<t:container xmlns:t='http://tapestry.apache.org/schema/tapestry_5_3.xsd'>
    <div class='accordion' id='sourceAccordion'>
        <t:loop t:source='sourceCodeResources' t:value='sourceCodeResource'
                t:index='index'>
            <t:if test='resourceExists'>
                <div class='accordion-group'>
                    <div class='accordion-heading'>

                        <a href='#Resource_${index}' class='accordion-toggle'
                           data-toggle='collapse'
                           data-parent='#sourceAccordion'>
                            ${sourceCodeResource.resource.path} </a>
                    </div>
                    <div id='Resource_${index}'
                         class='accordion-body collapse in'>


                        <pre class='${sourceCodeResource.type}'
                             name='Resource_${index}'>${source}</pre>


                    </div>
                </div>
            </t:if>
        </t:loop>
    </div>
</t:container>

Now let us apply some Tapestry magic to it. We can use class transformations to automatically add sourcecode for components/pages/mixins.


@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ShowSourceCode {

    /**
     * This method adds additional classes to be contributed to
     * {@link SourceCodeService}
     *
     * @return additional classes to be contributed
     */
    Class<?>[] additionalClasses() default {};

    /**
     * Resources for which source code to be attached.
     *
     * @return resources
     */
    SourceCode[] resources() default {};

    /**
     * Whether to use resources imported via {@link Import}
     * @return use import.
     */
    boolean useImport() default true;
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface SourceCode {

    /**
     * The resource path which uses the same path pattern as tapestry.
     * 
     * @return resource path.
     */
    String value();

    /**
     * Resource type.
     * 
     * @return resource type
     */
    String type() default "xml";

    /**
     * Is the source-code optional. If set to false and the resource is not
     * found an exception is thrown.
     * 
     * @return optional
     */
    boolean optional() default false;
}


So this annotation enables us to add source code in four different ways.

  1. Just annotating a component/page/mixins class with @ShowSourceCode to add their source code.
  2. Using @ShowSourceCode#useImport() to include source code from @Import annotation
  3. Use @ShowSourceCode#additionalClasses to include additional(non-component) classes
  4. Use @ShowSourceCode#resources to add additional resources

All this is done by the ComponentClassTransformWorker2 implementation

public class ShowSourceCodeWorker implements ComponentClassTransformWorker2 {

    private final SourceCodeService sourceCodeService;

    private final AssetSource assetSource;

    public ShowSourceCodeWorker(final SourceCodeService sourceCodeService,
            final AssetSource assetSource) {

        this.sourceCodeService = sourceCodeService;
        this.assetSource = assetSource;
    }

    @Override
    public void transform(final PlasticClass plasticClass,
            @SuppressWarnings("unused") final TransformationSupport support,
            final MutableComponentModel model) {

        if (plasticClass.hasAnnotation(ShowSourceCode.class)) {
            final PlasticMethod setupRender = plasticClass
                    .introduceMethod(TransformConstants.SETUP_RENDER_DESCRIPTION);
            final ShowSourceCode showSourceCode = plasticClass
                    .getAnnotation(ShowSourceCode.class);

            addAdvice(setupRender, model.getBaseResource(), showSourceCode,
                    plasticClass.getAnnotation(Import.class));

            model.addRenderPhase(SetupRender.class);


        }
    }

    private void addAdvice(final PlasticMethod method,
            final Resource baseResource, final ShowSourceCode showSourceCodeAnnotation,
            final Import importAnnotation) {

        method.addAdvice(new MethodAdvice() {

            @Override
            public void advise(final MethodInvocation invocation) {

                final ComponentResources resources = invocation
                        .getInstanceContext().get(ComponentResources.class);
                final Class<?> pageClass = invocation.getInstanceContext()
                        .getInstanceType();

                addComponent(pageClass);
                addAdditionalClasses(showSourceCodeAnnotation.additionalClasses());
                addResources(showSourceCodeAnnotation.resources(), resources.getLocale(),
                        baseResource);

                if (showSourceCodeAnnotation.useImport() && importAnnotation != null) {
                    addAssets(SourceCodeResource.JS, importAnnotation.library(), baseResource,
                            resources.getLocale());
                    addAssets(SourceCodeResource.CSS, importAnnotation.stylesheet(), baseResource,
                            resources.getLocale());
                }

                invocation.proceed();
            }

        });
    }

    private void addResources(final SourceCode[] sourceCodes,
            final Locale locale, final Resource baseResource) {

        for (final SourceCode sourceCode : sourceCodes) {
            this.sourceCodeService.add(new SourceCodeResource(
                    sourceCode.type(), this.assetSource.getAsset(baseResource,
                    sourceCode.value(), locale).getResource(),
                    sourceCode.optional()));
        }
    }

    private void addComponent(final Class<?> pageClass) {

        this.sourceCodeService.addComponent(pageClass);
    }

    private void addAdditionalClasses(final Class<?>[] additionalClasses) {

        for (final Class<?> clazz : additionalClasses) {
            this.sourceCodeService.add(clazz);
        }
    }

    private void addAssets(final String type, final String[] libraries,
            final Resource baseResource, final Locale locale) {

        for (final String library : libraries) {
            this.sourceCodeService.add(type, this.assetSource.getAsset(
                    baseResource, library, locale).getResource());
        }

    }
}

The worker is using SourceCodeService to add different resources . For components, we use the AssetSource to get the resources relative to the components.

Finally we have to contribute the ComponentClassTransformWorker2 in AppModule

public static void bind(final ServiceBinder binder) {

        binder.bind(SourceCodeService.class, SourceCodeServiceImpl.class)
                .scope(ScopeConstants.PERTHREAD);
    }

    @Contribute(ComponentClassTransformWorker2.class)
    public static void contributeWorkers(
            final OrderedConfiguration<ComponentClassTransformWorker2>
                    workers) {

        workers.addInstance("ShowSourceCode", ShowSourceCodeWorker.class,
                "after:RenderPhase,before:Import");
    }
}

Remember to copy the java source code. For maven2 you can do that by adding this to the pom.xml

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-source-plugin</artifactId>
    <version>2.1.2</version>
    <executions>
        <execution>
            <id>attach-sources</id>
            <phase>verify</phase>
            <goals>
                <goal>jar-no-fork</goal>
            </goals>
        </execution>
    </executions>
</plugin>

About these ads

Tagged: ,

2 thoughts on “Tapestry source code viewer

  1. juka August 13, 2012 at 8:39 PM Reply

    Hi! Thank you for example. I have one question though. Is it possible that ShowSourceCodeWorker implements ComponentClassTransformWorker? I have to use older version of Tapestry. Here is my edited ShowSourceCodeWorker class – http://pastebin.com/LkQk4WaC . The rest of the code is untouched. I get the nullpointer exception: Property ‘sourceCodeResource’ (within property expression ‘sourceCodeResource.resource.path’, of xxx.SourceCodeViewer@408f3be4) is null.

    • juka August 14, 2012 at 1:06 PM Reply

      Hello again, I fixed my problem. I’ve changed namespace to earlier version and got the other exception from which I realized that maven plugin did not copy source files to target directory. Thanks anyway.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 91 other followers

%d bloggers like this: