J2EE developer with over 7 years of experience in designing and implementing enterprise j2ee solutions based on open source technologies like Tapestry, Hibernate, Spring. Current interests include Tapestry, Plastic, Spock, Scala. Taha is a DZone MVB and is not an employee of DZone and has posted 40 posts at DZone. You can read more from them at their website. View Full User Profile

Tapestry Source Code Viewer

07.12.2012
| 2174 views |
  • submit to reddit

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>

 

 

 

Published at DZone with permission of Taha Siddiqi, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)