-
Notifications
You must be signed in to change notification settings - Fork 10
Using Generated AADL Property Getter Methods
We now have the ability to generate Java classes that simplify access to AADL property associations. This methods are meant to replace the ad hoc manner in which property associations were looked up in the past, and provide a uniform mechanism for manipulating the returned property values. The generated methods rely on a small set of supporting data type classes in the package org.osate.pluginsupport.properties in the project org.osate.pluginsupport.
Most of the existing property association lookups are performed using methods in GetProperties. A major problem with some of these methods is that they do more than just get the property value. They may also
- Attempt to get the value from multiple property associations
- Silently fall back on default values
- Convert the units of a numerical value
- Retrieve just the first element of a list value
- Perform similar trickery with range values
I have recently changed the flow analysis, bus load analysis, and several smaller packages to use the generated property getters for the standard AADL and SEI property sets. The packages for these getters are in the projects org.osate.aadl2.contrib and org.osate.contribution.sei, respectively.
Here I provide guidance on using the generated methods based on my experience.
The generated property lookup methods return java.lang.Optional objects, or in some cases the similar OptionalLong class. For example, from CommunicationProperties:
public static Optional<IntegerRangeWithUnits<TimeUnits>> getLatency(NamedElement lookupContext){ ... }
and
public static OptionalLong getQueueSize(NamedElement lookupContext) { ... }
These classes encapsulate the problem of a value only sometimes being available. This is appropriate for the property associations because the model element may not have an association for the given property. You can test if a value is available using isPresent(), and get the value using value(), which throws an exception if no value is present. These methods are rarely used directly, however. Instead the method map() or orElse() is usually the best choice to use; sometimes a combination of the two. The map() method has the following signature:
<U> Optional<U> map(Function<? super T, ? extends U> mapper)
If the Optional has a value, the value is passed to the given mapper function and the method's return value is returned wrapped as an Optional value. If the Optional doesn't have a value, map() returns an empty Optional.
The T orElse(T other) method returns the value if the optional has one, otherwise it returns the given value other. A variant T orElseGet(Supplier<? extends T> other) that takes a "thunk" can be used if the alternative a complicated expression that shouldn't be executed unless the optional is actually empty.
I've found that orElse is the perfect way to deal with providing default values for property lookups. For example, here we lookup up the Communication_Properties::Queue_Size property, and return 0 if the property association is missing:
final long queueSize = CommunicationProperties.getQueueSize(ne).orElse(0L);
While unnecessary for this case, here we show the same thing using orElseGet():
final long queueSize = CommunicationProperties.getQueueSize(ne).orElseGet(() -> 0L);
The most common operation performed on a property value is scale it. That is, most of the numeric property types have a unit, so the consumers of the property value always scale all the values to a common unit. This is a common enough operation that I created a utility method getScaled that both gets the property value and scales it. This method, and a few others, are in the class org.osate.pluginsupport.properties.PropertyUtils. The signature of the method is
<U extends Enum<U> & GeneratedUnits<U>> Optional<Double> getScaled(
Function<NamedElement, Optional<? extends Scalable<U>>> getProperty,
NamedElement ne, U unit)
The types are little strange because it is very generic:
-
<U>is an enumeration type that describes an AADL units type from the generated classes. -
getPropertyis a method handle for a method that takes aNamedElementand returns anOptionalvalue that uses the given units. Basically you pass a method handle of a generated property getter here. -
neis theNamedElementto get the property association from. -
unitis the enumeration element that describes the unit to scale the value to. The return value is anOptional<Double>of the scaled value. If the property value exists, it is returned scaled; if not,Optional.empty()is returned.
This seems complicated, but as you can see from the example below, the usage is very straightforward. Here we get the value of Timing_Properties::Period and scale the result to in terms of seconds.
double period = PropertyUtils.getScaled(
TimingProperties::getPeriod, threadinstance, TimeUnits.SEC).orElse(0.0);
As the returned object is an Optional, a typical usage is chained to the orElse() method to provide a default value.
(It is coincidental, but highly serendipitous, that both the AADL and Java syntax use the :: operator in this case.)
A similar method PropertyUtils.getScaledRange() works with property getter methods that return optional IntegerRangeWithUnits or RealRangeWithUnits values:
public static <U extends Enum<U> & GeneratedUnits<U>> Optional<RealRange> getScaledRange(
Function<NamedElement, Optional<? extends RangeWithUnits<U, ? extends Scalable<U>>>> getProperty,
NamedElement ne, U unit)
In this case an Optional<RealRange> is returned instead of an Optional<Double>. All the components of the range are scaled to the given unit.
The class org.osate.pluginsupport.properties.PropertyUtils contains a few other methods that are occasionally useful.
To make the above scaling methods as useful as possible, I had to tinker with the class hierarchy of the classes in
org.osate.pluginsupport.properties. As originally conceived, there as no type commonality that could be exploited among the different classes that support units. I added a Scalable interface as supertype to IntegerWithUnits and RealWithUnits, and a RangeWithUnits as supertype to both IntegerRangeWithUnits and RealRangeWithUnits:
public interface Scalable<U extends Enum<U> & GeneratedUnits<U>> {
public double getValue(U targetUnit);
}
public interface RangeWithUnits<U extends Enum<U> & GeneratedUnits<U>, T extends Scalable<U>> {
public T getMinimum();
public T getMaximum();
public Optional<T> getDelta();
}
(To be clear, I didn't need to add the getValue() method, or the getMinimum(), getMaximum(), and getDelta() methods. I only needed to explicitly capture the commonality in the new superinterfaces.)
A major difference in using the new generated methods versus the old way of doing things is how the case of an inapplicable property is handled. By this I mean the case where a property association is looked up on a model element to which the property does not apply based on the applies to clause of the property declaration. The underlying property lookup API throws an exception in this case. The methods in GetProperties are generally written to catch the exception and silenty return default values when the property does not apply. The generated property allow the PropertyDoesNotApplyToHolderException exception to propagate to the caller in this case, in general this makes more sense because the right thing to do depends on the context, but it makes updating the code more difficult because I've found that a lot of the code that uses GetProperties assumes the methods will do something sane regardless of what kind of model element they are given.
The upshot of this is that it before calling any of the generated property lookup methods the caller must make sure the model element is appropriate for the particular property. Often times this is implicit based on the nature of the computation. But in code that is meant to handle many different situations, it may be necessary to first explicitly test the "category" or "kind" of the model element. I've found that it is easiest to use EnumSet for this. For example, to guard a lookup of the Timing_Properties::Period property, I first create a set
private final static Set<ComponentCategory> hasPeriod = EnumSet.of(ComponentCategory.THREAD,
ComponentCategory.THREAD_GROUP, ComponentCategory.PROCESS, ComponentCategory.SYSTEM,
ComponentCategory.DEVICE, ComponentCategory.VIRTUAL_PROCESSOR, ComponentCategory.VIRTUAL_BUS,
ComponentCategory.BUS, ComponentCategory.ABSTRACT);
The code performing the property lookup on a ComponentInstance or ComponentClassifier ne would be guarded by
final ComponentCategory cc = ne instanceof ComponentInstance
? ((ComponentInstance) ne).getCategory() : ((ComponentClassifier) ne).getCategory();
if (hasPeriod.contains(cc)) {
// continued to lookup property on ne
}
This trick can also be used for instances of FeatureInstance, which have a getCategory() method that returns an element of the FeatureCategory enumeration.
The elements of the set are informed by the applies to clause of the property definition. One thing to be careful of, is that any property that applies to a component can apply to an abstract component. This is not the case, however, for abstract features. A property only applies to an abstract feature if it is explicitly declared to. (This is an inconsistency in the AADL standard.).
Most of the existing uses of properties are handled by methods in GetProperties. These can usually be replaced by straightforward calls sequences of the form
double period = PropertyUtils.getScaled(
TimingProperties::getPeriod, threadinstance, TimeUnits.SEC).orElse(0.0);
In some more complicated cases, it makes more sense to add a helper method to the class needing the operation. For example, in class NewBusLoadAnalysis I added the method
private static double getConnectionActualKBytesps(final ConnectionInstanceEnd cie,
final double dataOverheadKBytes) {
double actualDataRate = 0;
if (cie instanceof FeatureInstance) {
final FeatureInstance fi = (FeatureInstance) cie;
final double datasize = dataOverheadKBytes
+ PropertyUtils.getScaled(MemoryProperties::getDataSize, fi, SizeUnits.KBYTE).orElseGet(
() -> PropertyUtils.getScaled(MemoryProperties::getSourceDataSize, fi, SizeUnits.KBYTE)
.orElse(0.0));
final double srcRate = getOutgoingMessageRatePerSecond(fi);
actualDataRate = datasize * srcRate;
}
return actualDataRate;
}
A few helper methods are generic enough and useful enough that I have added them the class org.osate.aadl2.contrib.util.AadlContribUtils.
There are a few places in the flow analysis where the analysis explicitly tests whether a property value is set using GetProperties.isAssignedPropertyValue(). This method is implemented as
public static boolean isAssignedPropertyValue(NamedElement element, Property pn) {
try {
final PropertyAcc propertyAccumulator = element.getPropertyValue(pn);
PropertyAssociation firstAssociation = propertyAccumulator.first();
return firstAssociation != null;
} catch (org.osate.aadl2.properties.PropertyDoesNotApplyToHolderException exception) {
return false;
}
}
In these cases the analysis trying to bypass the default property value that AADL semantics say should be used when the property value is not explicitly set. It's not currently clear to me why this is being done. There is no good way to emulate this behavior using the generated property getter methods. (Nor should we try to.)