Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.swirlds.common.wiring.component.ComponentWiring Maven / Gradle / Ivy
Go to download
Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* 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 com.swirlds.common.wiring.component;
import static com.swirlds.common.wiring.model.diagram.HyperlinkBuilder.platformCoreHyperlink;
import com.swirlds.common.wiring.component.internal.FilterToBind;
import com.swirlds.common.wiring.component.internal.InputWireToBind;
import com.swirlds.common.wiring.component.internal.TransformerToBind;
import com.swirlds.common.wiring.component.internal.WiringComponentProxy;
import com.swirlds.common.wiring.model.WiringModel;
import com.swirlds.common.wiring.schedulers.TaskScheduler;
import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerConfiguration;
import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType;
import com.swirlds.common.wiring.transformers.RoutableData;
import com.swirlds.common.wiring.transformers.WireFilter;
import com.swirlds.common.wiring.transformers.WireRouter;
import com.swirlds.common.wiring.transformers.WireTransformer;
import com.swirlds.common.wiring.wires.input.BindableInputWire;
import com.swirlds.common.wiring.wires.input.InputWire;
import com.swirlds.common.wiring.wires.output.OutputWire;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Builds and manages input/output wires for a component.
*
* @param the type of the component
* @param the output type of the component
*/
@SuppressWarnings("unchecked")
public class ComponentWiring {
private final WiringModel model;
private final TaskScheduler scheduler;
private final WiringComponentProxy proxy = new WiringComponentProxy();
private final COMPONENT_TYPE proxyComponent;
/**
* The component that implements the business logic. Will be null until {@link #bind(Object)} is called.
*/
private COMPONENT_TYPE component;
/**
* Input wires that have been created for this component.
*/
private final Map> inputWires = new HashMap<>();
/**
* Input wires that need to be bound.
*/
private final List> inputsToBind = new ArrayList<>();
/**
* Previously created transformers/splitters/filters.
*/
private final Map> alternateOutputs = new HashMap<>();
/**
* Transformers that need to be bound.
*/
private final List> transformersToBind = new ArrayList<>();
/**
* Filters that need to be bound.
*/
private final List> filtersToBind = new ArrayList<>();
/**
* A splitter (if one has been constructed).
*/
private OutputWire splitterOutput;
/**
* A router (if one has been constructed).
*/
private WireRouter> router;
/**
* A router that consumes the output of a splitter (if one has been constructed).
*/
private WireRouter> splitRouter;
/**
* Create a new component wiring.
*
* @param model the wiring model that will contain the component
* @param clazz the interface class of the component
* @param scheduler the task scheduler that will run the component
* @deprecated use {@link #ComponentWiring(WiringModel, Class, TaskSchedulerConfiguration)} instead. Once all uses
* have been updated, this constructor will be removed.
*/
@Deprecated
public ComponentWiring(
@NonNull final WiringModel model,
@NonNull final Class clazz,
@NonNull final TaskScheduler scheduler) {
this.model = Objects.requireNonNull(model);
this.scheduler = Objects.requireNonNull(scheduler);
if (!clazz.isInterface()) {
throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface.");
}
proxyComponent = (COMPONENT_TYPE) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, proxy);
}
/**
* Create a new component wiring.
*
* @param model the wiring model that will contain the component
* @param clazz the interface class of the component
* @param schedulerConfiguration for the task scheduler that will run the component
*/
public ComponentWiring(
@NonNull final WiringModel model,
@NonNull final Class clazz,
@NonNull final TaskSchedulerConfiguration schedulerConfiguration) {
this.model = Objects.requireNonNull(model);
Objects.requireNonNull(schedulerConfiguration);
final String schedulerName;
final SchedulerLabel schedulerLabelAnnotation = clazz.getAnnotation(SchedulerLabel.class);
if (schedulerLabelAnnotation == null) {
schedulerName = clazz.getSimpleName();
} else {
schedulerName = schedulerLabelAnnotation.value();
}
this.scheduler = model.schedulerBuilder(schedulerName)
.configure(schedulerConfiguration)
// FUTURE WORK: all components not currently in platform core should move there
.withHyperlink(platformCoreHyperlink(clazz))
.build()
.cast();
if (!clazz.isInterface()) {
throw new IllegalArgumentException("Component class " + clazz.getName() + " is not an interface.");
}
proxyComponent = (COMPONENT_TYPE) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] {clazz}, proxy);
}
/**
* Get the output wire of this component.
*
* @return the output wire
*/
@NonNull
public OutputWire getOutputWire() {
return scheduler.getOutputWire();
}
/**
* Get an input wire for this component.
*
* @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
* method on the class, not a method on a specific instance.
* @param the type of the input
* @return the input wire
*/
public InputWire getInputWire(
@NonNull final BiFunction handler) {
Objects.requireNonNull(handler);
try {
handler.apply(proxyComponent, null);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types or return types. Use a boxed primitive "
+ "instead.",
e);
}
return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), handler, null, null, null);
}
/**
* Get an input wire for this component.
*
* @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
* method on the class, not a method on a specific instance.
* @param the input type
* @return the input wire
*/
public InputWire getInputWire(
@NonNull final BiConsumer handler) {
Objects.requireNonNull(handler);
try {
handler.accept(proxyComponent, null);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
}
return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, handler, null, null);
}
/**
* Get an input wire for this component.
*
* @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
* method on the class, not a method on a specific instance.
* @param the input type
* @return the input wire
*/
@NonNull
public InputWire getInputWire(
@NonNull final Function handler) {
Objects.requireNonNull(handler);
try {
handler.apply(proxyComponent);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
}
return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, null, handler, null);
}
/**
* Get an input wire for this component.
*
* @param handler the component method that will handle the input, e.g. "MyComponent::handleInput". Should be a
* method on the class, not a method on a specific instance.
* @param the input type
* @return the input wire
*/
@NonNull
public InputWire getInputWire(@NonNull final Consumer handler) {
Objects.requireNonNull(handler);
try {
handler.accept(proxyComponent);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types. Use a boxed primitive instead.", e);
}
return getOrBuildInputWire(proxy.getMostRecentlyInvokedMethod(), null, null, null, handler);
}
/**
* Get the output wire of this component, transformed by a function.
*
* @param transformation the function that will transform the output, must be a static method on the component
* @param the type of the transformed output
* @return the transformed output wire
*/
@NonNull
public OutputWire getTransformedOutput(
@NonNull final BiFunction transformation) {
return getOrBuildTransformer(transformation, getOutputWire());
}
/**
* Get the output wire of a splitter of this component, transformed by a function. Automatically constructs the
* splitter if it does not already exist. Intended for use only with components that produce lists of items.
*
* @param transformation the function that will transform the output, must be a static method on the component
* @param the type of the elements in the list, the base type of this component's output is expected
* to be a list of this type
*/
public OutputWire getSplitAndTransformedOutput(
@NonNull final BiFunction transformation) {
return getOrBuildTransformer(transformation, getSplitOutput());
}
/**
* Create a filter for the output of this component.
*
* @param predicate the filter predicate
* @return the output wire of the filter
*/
@NonNull
public OutputWire getFilteredOutput(
@NonNull final BiFunction predicate) {
return getOrBuildFilter(predicate, getOutputWire());
}
/**
* Create a filter for the output of a splitter of this component. Automatically constructs the splitter if it does
* not already exist. Intended for use only with components that produce lists of items.
*
* @param predicate the filter predicate
* @param the type of the elements in the list, the base type of this component's output is expected to be
* a list of this type
* @return the output wire of the filter
*/
@NonNull
public OutputWire getSplitAndFilteredOutput(
@NonNull final BiFunction predicate) {
return getOrBuildFilter(predicate, getSplitOutput());
}
/**
* Create a splitter for the output of this component. A splitter converts an output wire that produces lists of
* items into an output wire that produces individual items. Note that calling this method on a component that does
* not produce lists will result in a runtime exception.
*
* @param the type of the elements in the list, the base type of this component's output is expected to be
* a list of this type
* @return the output wire
*/
@NonNull
public OutputWire getSplitOutput() {
if (splitterOutput == null) {
// Future work: there is not a clean way to specify the "splitterInputName" label, so as a short
// term work around we can just call it "data". This is ugly but ok as a temporary place holder.
// The proper way to fix this is to change the way we assign labels to wires in the diagram.
// Instead of defining names for input wires, we should instead define names for output wires,
// and require that any scheduler that has output define the label for its output data.
splitterOutput = getOutputWire().buildSplitter(scheduler.getName() + "Splitter", "data");
}
return (OutputWire) splitterOutput;
}
/**
* Get an output wire that will emit a specific type of routed data. Data routing is when a component has multiple
* outputs, and different things want to receive a subset of that output. Each output is described by a routing
* address, which are implemented by enum values.
*
* This method should only be used for components which have an output type of
* {@link com.swirlds.common.wiring.transformers.RoutableData RoutableData}. Calling this method more than once with
* different enum classes will throw.
*
* @param address an enum value that describes one of the different types of data that can be routed
* @param the enum that describes the different types of data handled by this router
* @param the type of data that travels over the output wire
* @return the output wire
*/
@NonNull
public , DATA_TYPE> OutputWire getRoutedOutput(
@NonNull final ROUTER_ENUM address) {
final Class clazz = (Class) address.getClass();
return getOrBuildRouter(clazz).getOutput(address);
}
/**
* Get an output wire that will receive a specific type of routed data after being split apart. Data routing is when
* a component has multiple outputs, and different things want to receive a subset of that output. Each output is
* described by a routing address, which are implemented by enum values.
*
* This method should only be used for components which have an output type of
* {@link com.swirlds.common.wiring.transformers.RoutableData List<RoutableData>}. Calling this method more
* than once with different enum classes will throw.
*
* @param address an enum value that describes one of the different types of data that can be routed
* @param the enum that describes the different types of data handled by this router
* @param the type of data that travels over the output wire
* @return the output wire
*/
@NonNull
public , DATA_TYPE> OutputWire getSplitAndRoutedOutput(
@NonNull final ROUTER_ENUM address) {
final Class clazz = (Class) address.getClass();
return getOrBuildSplitRouter(clazz).getOutput(address);
}
/**
* Get the router for this component if one has been built. Build and return a new router if one has not been
* built.
*
* @param routerType the type of the router
* @param the type of the router
* @return the router
*/
@NonNull
private > WireRouter getOrBuildRouter(
@NonNull final Class routerType) {
if (splitRouter != null) {
throw new IllegalStateException("Only one type of router can be constructed per task scheduler. "
+ "This task scheduler already has a router that was built using the split output type, "
+ "so a router cannot be created with the unmodified output type.");
}
if (router != null) {
if (!router.getRouterType().equals(routerType)) {
throw new IllegalArgumentException("Only one type of router can be constructed per task scheduler. "
+ "This task scheduler already has a router of type "
+ router.getRouterType().getName() + "but an attempt was made to construct a router of type "
+ routerType.getName());
}
} else {
router = new WireRouter<>(model, getSchedulerName() + "Router", "data", routerType);
getOutputWire().solderTo((InputWire) router.getInput());
}
return (WireRouter) router;
}
/**
* Get the router for split data for this component if one has been built. Build and return a new splitter and
* router if one has not been built.
*
* @param routerType the type of the router
* @param the type of the router
* @return the router
*/
@NonNull
private > WireRouter getOrBuildSplitRouter(
@NonNull final Class routerType) {
if (router != null) {
throw new IllegalStateException("Only one type of router can be constructed per task scheduler. "
+ "This task scheduler already has a router that was built using the unmodified output type, "
+ "so a router cannot be created with the split output type.");
}
if (splitRouter != null) {
if (!splitRouter.getRouterType().equals(routerType)) {
throw new IllegalArgumentException("Only one type of router can be constructed per task scheduler. "
+ "This task scheduler already has a router of type "
+ router.getRouterType().getName() + "but an attempt was made to construct a router of type "
+ routerType.getName());
}
} else {
splitRouter = new WireRouter<>(model, getSchedulerName() + "Router", "data", routerType);
final OutputWire> splitOutput = getSplitOutput();
final InputWire> routerInput = ((WireRouter) splitRouter).getInput();
splitOutput.solderTo(routerInput);
}
return (WireRouter) splitRouter;
}
/**
* Create a transformed output wire or return the existing one if it has already been created.
*
* @param transformation the function that will transform the output, must be a static method on the component
* @param transformerSource the source of the data to transform (i.e. the base output wire or the output wire of
* the splitter)
* @param the type of the elements passed to the transformer
* @param the type of the transformed output
* @return the transformed output wire
*/
@NonNull
private OutputWire getOrBuildTransformer(
@NonNull final BiFunction transformation,
@NonNull final OutputWire transformerSource) {
Objects.requireNonNull(transformation);
try {
transformation.apply(proxyComponent, null);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types or return types. "
+ "Use a boxed primitive instead.",
e);
}
final Method method = proxy.getMostRecentlyInvokedMethod();
if (!method.isDefault()) {
throw new IllegalArgumentException("Method " + method.getName() + " does not have a default.");
}
if (alternateOutputs.containsKey(method)) {
// We've already created this transformer.
return (OutputWire) alternateOutputs.get(method);
}
final String wireLabel;
final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class);
if (inputWireLabel == null) {
wireLabel = "data to transform";
} else {
wireLabel = inputWireLabel.value();
}
final String schedulerLabel;
final SchedulerLabel schedulerLabelAnnotation = method.getAnnotation(SchedulerLabel.class);
if (schedulerLabelAnnotation == null) {
schedulerLabel = method.getName();
} else {
schedulerLabel = schedulerLabelAnnotation.value();
}
final WireTransformer transformer =
new WireTransformer<>(model, schedulerLabel, wireLabel);
transformerSource.solderTo(transformer.getInputWire());
alternateOutputs.put(method, transformer.getOutputWire());
if (component == null) {
// we will bind this later
transformersToBind.add((TransformerToBind)
new TransformerToBind<>(transformer, transformation));
} else {
// bind this now
transformer.bind(x -> transformation.apply(component, x));
}
return transformer.getOutputWire();
}
/**
* Create a filtered output wire or return the existing one if it has already been created.
*
* @param predicate the filter predicate
* @param filterSource the source of the data to filter (i.e. the base output wire or the output wire of the
* splitter)
* @param the type of the elements passed to the filter
* @return the output wire of the filter
*/
private OutputWire getOrBuildFilter(
@NonNull final BiFunction predicate,
@NonNull final OutputWire filterSource) {
Objects.requireNonNull(predicate);
try {
predicate.apply(proxyComponent, null);
} catch (final NullPointerException e) {
throw new IllegalStateException(
"Component wiring does not support primitive input types or return types. "
+ "Use a boxed primitive instead.",
e);
}
final Method method = proxy.getMostRecentlyInvokedMethod();
if (!method.isDefault()) {
throw new IllegalArgumentException("Method " + method.getName() + " does not have a default.");
}
if (alternateOutputs.containsKey(method)) {
// We've already created this filter.
return (OutputWire) alternateOutputs.get(method);
}
final String wireLabel;
final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class);
if (inputWireLabel == null) {
wireLabel = "data to filter";
} else {
wireLabel = inputWireLabel.value();
}
final String schedulerLabel;
final SchedulerLabel schedulerLabelAnnotation = method.getAnnotation(SchedulerLabel.class);
if (schedulerLabelAnnotation == null) {
schedulerLabel = method.getName();
} else {
schedulerLabel = schedulerLabelAnnotation.value();
}
final WireFilter filter = new WireFilter<>(model, schedulerLabel, wireLabel);
filterSource.solderTo(filter.getInputWire());
alternateOutputs.put(method, filter.getOutputWire());
if (component == null) {
// we will bind this later
filtersToBind.add((FilterToBind) new FilterToBind<>(filter, predicate));
} else {
// bind this now
filter.bind(x -> predicate.apply(component, x));
}
return filter.getOutputWire();
}
/**
* Get the input wire for a specified method.
*
* @param method the method that will handle data on the input wire
* @param handlerWithReturn the handler for the method if it has a return type
* @param handlerWithoutReturn the handler for the method if it does not have a return type
* @param handlerWithoutParameter the handler for the method if it does not have a parameter
* @param handlerWithoutReturnAndWithoutParameter the handler for the method if it does not have a return type and
* does not have a parameter
* @param the input type
* @return the input wire
*/
private InputWire getOrBuildInputWire(
@NonNull final Method method,
@Nullable final BiFunction handlerWithReturn,
@Nullable final BiConsumer handlerWithoutReturn,
@Nullable final Function handlerWithoutParameter,
@Nullable final Consumer handlerWithoutReturnAndWithoutParameter) {
if (inputWires.containsKey(method)) {
// We've already created this wire
return (InputWire) inputWires.get(method);
}
final String label;
final InputWireLabel inputWireLabel = method.getAnnotation(InputWireLabel.class);
if (inputWireLabel == null) {
label = method.getName();
} else {
label = inputWireLabel.value();
}
final BindableInputWire inputWire = scheduler.buildInputWire(label);
inputWires.put(method, (BindableInputWire) inputWire);
if (component == null) {
// we will bind this later
inputsToBind.add((InputWireToBind) new InputWireToBind<>(
inputWire,
handlerWithReturn,
handlerWithoutReturn,
handlerWithoutParameter,
handlerWithoutReturnAndWithoutParameter));
} else {
// bind this now
if (handlerWithReturn != null) {
inputWire.bind(x -> handlerWithReturn.apply(component, x));
} else if (handlerWithoutReturn != null) {
inputWire.bindConsumer(x -> {
handlerWithoutReturn.accept(component, x);
});
} else if (handlerWithoutParameter != null) {
inputWire.bind(x -> handlerWithoutParameter.apply(component));
} else {
assert handlerWithoutReturnAndWithoutParameter != null;
inputWire.bindConsumer(x -> {
handlerWithoutReturnAndWithoutParameter.accept(component);
});
}
}
return inputWire;
}
/**
* Flush all data in the task scheduler. Blocks until all data currently in flight has been processed.
*
* @throws UnsupportedOperationException if the scheduler does not support flushing
*/
public void flush() {
scheduler.flush();
}
/**
* Start squelching the output of this component.
*
* @throws UnsupportedOperationException if the scheduler does not support squelching
* @throws IllegalStateException if the scheduler is already squelching
*/
public void startSquelching() {
scheduler.startSquelching();
}
/**
* Stop squelching the output of this component.
*
* @throws UnsupportedOperationException if the scheduler does not support squelching
* @throws IllegalStateException if the scheduler is not squelching
*/
public void stopSquelching() {
scheduler.stopSquelching();
}
/**
* Bind the component to the input wires.
*
* @param component the component to bind
*/
public void bind(@NonNull final COMPONENT_TYPE component) {
Objects.requireNonNull(component);
this.component = component;
// Bind input wires
for (final InputWireToBind wireToBind : inputsToBind) {
if (wireToBind.handlerWithReturn() != null) {
final BiFunction handlerWithReturn =
(BiFunction) wireToBind.handlerWithReturn();
wireToBind.inputWire().bind(x -> handlerWithReturn.apply(component, x));
} else if (wireToBind.handlerWithoutReturn() != null) {
final BiConsumer handlerWithoutReturn =
(BiConsumer) wireToBind.handlerWithoutReturn();
wireToBind.inputWire().bindConsumer(x -> {
handlerWithoutReturn.accept(component, x);
});
} else if (wireToBind.handlerWithoutParameter() != null) {
wireToBind
.inputWire()
.bind(x -> wireToBind.handlerWithoutParameter().apply(component));
} else {
assert wireToBind.handlerWithoutReturnAndWithoutParameter() != null;
wireToBind.inputWire().bindConsumer(x -> {
wireToBind.handlerWithoutReturnAndWithoutParameter().accept(component);
});
}
}
// Bind transformers
for (final TransformerToBind transformerToBind : transformersToBind) {
final WireTransformer transformer = transformerToBind.transformer();
final BiFunction transformation = transformerToBind.transformation();
transformer.bind(x -> transformation.apply(component, x));
}
// Bind filters
for (final FilterToBind filterToBind : filtersToBind) {
filterToBind.filter().bind(x -> filterToBind.predicate().apply(component, x));
}
}
/**
* Bind to a component. This method is similar to {@link #bind(Object)}, but it allows the component to be created
* if and only if we need to bind to it. This method will invoke the supplier if the task scheduler type is anything
* other than a {@link com.swirlds.common.wiring.schedulers.builders.TaskSchedulerType#NO_OP NO_OP} scheduler.
*
* @param componentBuilder builds or supplies the component
*/
public void bind(@NonNull final Supplier componentBuilder) {
Objects.requireNonNull(componentBuilder);
if (scheduler.getType() != TaskSchedulerType.NO_OP) {
bind(componentBuilder.get());
}
}
/**
* Get the name of the scheduler that is running this component.
*
* @return the name of the scheduler
*/
@NonNull
public String getSchedulerName() {
return scheduler.getName();
}
}