Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2026 The Apache Software Foundation
//
// Licensed 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.tapestry5.corelib.pages;

import java.util.List;

import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
import org.apache.tapestry5.annotations.WhitelistAccessOnly;
import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.AssembledPipeline;
import org.apache.tapestry5.ioc.services.PipelineBuilder;

/**
* Page used to see the pipelines assembled by the {@link PipelineBuilder}: which filters are in each pipeline, and
* in which order they are invoked.
*
* A pipeline is assembled when the service it implements is first realized, so this page shows more as the
* application runs.
*
* @since 5.10
*/
@UnknownActivationContextCheck(false)
@WhitelistAccessOnly
public class Pipelines
{
@Inject
private PipelineBuilder pipelineBuilder;

@Property
@Inject
@Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE)
private boolean productionMode;

@Property
private AssembledPipeline pipeline;

@Property
private int pipelineIndex;

@Property
private String filter;

@Cached
public List<AssembledPipeline> getPipelines()
{
return pipelineBuilder.getAssembledPipelines();
}

/**
* Identifies the panel of the current pipeline, for the links at the top of the page. The index is used rather
* than the service interface, since the same interface may be the basis of more than one pipeline.
*/
public String getPipelineClientId()
{
return "pipeline-" + pipelineIndex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static void defaultTabs(OrderedConfiguration<DashboardTab> configuration,
{
configuration.add("Pages", new DashboardTab("Pages", "core/PageCatalog"));
configuration.add("Services", new DashboardTab("Services", "core/ServiceStatus"));
configuration.add("Pipelines", new DashboardTab("Pipelines", "core/Pipelines"));
configuration.add("Libraries", new DashboardTab("ComponentLibraries", "core/ComponentLibraries"));
configuration.add("PageDependencyGraph", new DashboardTab("PageDependencyGraph", "core/PageDependencyGraph"));
if (!productionMode && multipleClassLoaders)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<t:block id="content"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" xmlns:p="tapestry:parameter">

<t:unless test="productionMode">
<p:then>
<p>
<strong>${pipelines.size()}</strong> pipelines assembled so far.
A pipeline is assembled when the service it implements is first realized,
so this list grows as the application runs.
</p>
<ul class="list-group">
<li class="list-group-item" t:type="Loop" t:source="pipelines" t:value="pipeline"
index="pipelineIndex">
<span class="badge">${pipeline.filters.size()}</span>
<a href="#${pipelineClientId}">
<code>${pipeline.serviceInterface.simpleName}</code>
</a>
</li>
</ul>

<div class="panel panel-default" t:type="Loop" t:source="pipelines" t:value="pipeline"
index="pipelineIndex" id="${pipelineClientId}">

<div class="panel-heading">
<code>${pipeline.serviceInterface.name}</code>
</div>

<div class="panel-body">
<p>
Filtered by: <code>${pipeline.filterInterface.name}</code>
</p>

<t:if test="pipeline.filters.empty">
<p:then>
<p class="text-muted">No filters contributed; the pipeline is just its terminator.</p>
</p:then>
<p:else>
<ol>
<li t:type="Loop" t:source="pipeline.filters" t:value="filter">
<code>${filter}</code>
</li>
</ol>
</p:else>
</t:if>

<p>
Terminator:
<code>${pipeline.terminator}</code>
</p>
</div>
</div>
</p:then>
<p:else>
<p>
<em>The filters of each pipeline are only shown in development mode.</em>
</p>
</p:else>
</t:unless>

</t:block>
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.apache.tapestry5.corelib.mixins.RenderDisabled;
import org.apache.tapestry5.integration.app1.pages.RenderErrorDemo;
import org.apache.tapestry5.services.ComponentRequestHandler;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -743,6 +744,21 @@ public void services_status()
assertTextPresent("services defined in the IoC Registry");
}

/**
* This basically checks that the pipelines page does not error.
*/
@Test
public void pipelines()
{
open(getBaseURL() + "t5dashboard/pipelines");

assertTextPresent("pipelines assembled so far");

// Every request goes through this pipeline, so it has certainly been assembled by now.

assertTextPresent(ComponentRequestHandler.class.getName());
}

/**
* Tests TAPESTRY-1934.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@
*/
public class BridgeBuilder<S, F>
{
private static final MethodDescription GET_PIPELINE_FILTER = new MethodDescription("java.lang.Object",
"getPipelineFilter");

private static final MethodDescription GET_PIPELINE_NEXT = new MethodDescription("java.lang.Object",
"getPipelineNext");

private final Logger logger;

private final Class<S> serviceInterface;

private final Class<F> filterInterface;

private final String header;

private final FilterMethodAnalyzer filterMethodAnalyzer;

private final PlasticProxyFactory proxyFactory;
Expand All @@ -47,6 +55,8 @@ public BridgeBuilder(Logger logger, Class<S> serviceInterface, Class<F> filterIn

this.proxyFactory = proxyFactory;

header = PipelineDescriber.header(serviceInterface, filterInterface);

filterMethodAnalyzer = new FilterMethodAnalyzer(serviceInterface);
}

Expand Down Expand Up @@ -80,8 +90,75 @@ public void transform(PlasticClass plasticClass)

processMethods(plasticClass, filterField, nextField);

plasticClass.addToString(String.format("<PipelineBridge from %s to %s>", serviceInterface.getName(),
filterInterface.getName()));
introducePipelineStep(plasticClass, filterField, nextField);
}
});
}

/**
* Makes the bridge a {@link PipelineStep}, exposing the filter it wraps and what it delegates to, and adds a
* {@code toString()} that describes the pipeline from this step onwards.
*/
private void introducePipelineStep(PlasticClass plasticClass, final PlasticField filterField,
final PlasticField nextField)
{
// Rule out that a service interface declares one of the methods we want to introduce.
// Fall back to a plain description.

if (plasticClass.isMethodImplemented(GET_PIPELINE_FILTER) || plasticClass.isMethodImplemented(GET_PIPELINE_NEXT))
{
plasticClass.addToString(header);

return;
}

plasticClass.introduceInterface(PipelineStep.class);

plasticClass.introduceMethod(GET_PIPELINE_FILTER, new InstructionBuilderCallback()
{
@Override
public void doBuild(InstructionBuilder builder)
{
builder.loadThis().getField(filterField).returnResult();
}
});

plasticClass.introduceMethod(GET_PIPELINE_NEXT, new InstructionBuilderCallback()
{
@Override
public void doBuild(InstructionBuilder builder)
{
builder.loadThis().getField(nextField).returnResult();
}
});

addToString(plasticClass);
}

/**
* Adds a {@code toString()} that delegates to
* {@link PipelineDescriber#describe(String, PipelineStep)}, describing the whole chain from this bridge onwards.
*
* Nothing is added when {@code toString()} is part of the service interface itself; in that case the method is
* already bridged to the filter, and that takes precedence. The pipeline can still be walked as a
* {@link PipelineStep}.
*/
private void addToString(PlasticClass plasticClass)
{
if (plasticClass.isMethodImplemented(PlasticUtils.TO_STRING_DESCRIPTION))
return;

plasticClass.introduceMethod(PlasticUtils.TO_STRING_DESCRIPTION, new InstructionBuilderCallback()
{
@Override
public void doBuild(InstructionBuilder builder)
{
builder.loadConstant(header).loadThis();

builder.invokeStatic(PipelineDescriber.class, String.class, "describe", String.class,
PipelineStep.class);

builder.returnResult();
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

package org.apache.tapestry5.ioc.internal.services;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.tapestry5.commons.services.PlasticProxyFactory;
import org.apache.tapestry5.commons.util.CollectionFactory;
import org.apache.tapestry5.ioc.services.AssembledPipeline;
import org.apache.tapestry5.ioc.services.Builtin;
import org.apache.tapestry5.ioc.services.DefaultImplementationBuilder;
import org.apache.tapestry5.ioc.services.PipelineBuilder;
Expand All @@ -29,6 +33,8 @@ public class PipelineBuilderImpl implements PipelineBuilder

private final DefaultImplementationBuilder defaultImplementationBuilder;

private final List<AssembledPipeline> assembledPipelines = CollectionFactory.newThreadSafeList();

public PipelineBuilderImpl(@Builtin
PlasticProxyFactory proxyFactory,

Expand All @@ -50,6 +56,8 @@ public <S, F> S build(Logger logger, Class<S> serviceInterface, Class<F> filterI
public <S, F> S build(Logger logger, Class<S> serviceInterface, Class<F> filterInterface, List<F> filters,
S terminator)
{
track(serviceInterface, filterInterface, filters, terminator);

if (filters.isEmpty())
return terminator;

Expand All @@ -69,7 +77,35 @@ public <S, F> S build(Logger logger, Class<S> serviceInterface, Class<F> filterI
next = bb.instantiateBridge(next, filter);
}

if (logger.isDebugEnabled())
{
logger.debug("Assembled pipeline: {}", next);
}

return next;
}

@Override
public List<AssembledPipeline> getAssembledPipelines()
{
return Collections.unmodifiableList(assembledPipelines);
}

/**
* Records the pipeline for later introspection, describing the filters and the terminator as they are now rather
* than holding on to them.
*/
private <S, F> void track(Class<S> serviceInterface, Class<F> filterInterface, List<F> filters, S terminator)
{
List<String> descriptions = new ArrayList<>(filters.size());

for (F filter : filters)
{
descriptions.add(String.valueOf(filter));
}

assembledPipelines.add(new AssembledPipeline(serviceInterface, filterInterface, descriptions,
String.valueOf(terminator)));
}

}
Loading