Bean identities
Bean identities are properties of the beans that unmistakably identifies the entity represented by the bean. It can be considered an abstraction of the primary key in the ER paradigm.
Interface Identity
provides an abstract mechanism to retrieve and manipulate
entity identities with zero knowledge of the composition of the identity.
Useful for referencing entities in front-ends and REST services without
exposing or depending of the implementation of the entities in the back-end
layer.
A default set of single value identity implementations is provided, including implementations for identities of the next types:
Long
(LongIdentity
)String
(StringIdentity
)BigInteger
(BigIntegerIdentity
)
The interface provides method getIdentityToken()
, which returns a
String
representation of the identity. This token must be used when exposed
through APIs, and should be a valid identifier in most of the contexts.
Token identity
When a identity is received through APIs the token should be converted to an
implementation of TokenIdentity
, which represents an identity of unknown
type.
Only services directly responsible of identity composition (like DAOs) should
try to resolve the generic TokenIdentity
to the required implementation.
Other components should use the passed identity “as is”.
To resolve TokenIdentity
instances to specific identity implementations
the method Identity.resolve(Class)
and the class IdentityResolver
is provided.
This class tries to create an instance of the requested identity type based on
the target class configuration (see Custom identities):
TokenIdentity unresolvedIdentity;
// Through Identity interface
MyIdentity resolvedIdentity = unresolvedIdentity.resolve(
MyIdentity.class);
assertEquals(
unresolvedIdentity.getIdentityToken(),
resolvedIdentity.getIdentityToken());
// Through IdentityResolver utility class
IdentityResolver resolver = IdentityResolver.getInstance();
MyIdentity resolvedIdentity = resolver.resolve(
unresolvedIdentity,
MyIdentity.class);
assertEquals(
unresolvedIdentity.getIdentityToken(),
resolvedIdentity.getIdentityToken());
Custom identities
To implement custom identities abstract classes AbstractIdentity
,
AbstractSimpleIdentity
and AbstractComposedIdentity
are provided.
A custom identity must provide a identity body through method
getIdentityTokenBody()
that represents the identity components as String
and a method to parse a identity token back to the identity type.
Class AbstractSimpleIdentity
provides base methods to identities composed by
a single value.
Class AbstractComposedIdentity
provides base methods to identities composed
by a multiple values.
To implement the token parsing method a constructor with a single String
argument is allowed. For implementations that cannot provide such a
constructor or prefer another method a public static method that accepts a
single String
parameter annotated with IdentityTokenResolver
can be provided:
class MyIdentity
extends AbstractSimpleIdentity<CustomType> {
/**
* Creates a new instance.
*
* @param value The identity value
*/
public MyIdentity(
final CustomType value) {
super(value);
}
/**
* Copy constructor.
*
* @param copy The instance to copy
*/
public MyIdentity(
final @NotNull MyIdentity copy) {
super(copy);
}
/**
* {@inheritDoc}
*/
@Override
protected String getIdentityTokenBody() {
// Implemented by AbstractSimpleIdentity. Can be overwritten
// to customize String conversion.
return this.getValue() == null ? null : this.getValue().toString();
}
/**
* Resolves the specified identity token to a valid {@code MyIdentity}
*
* @param token The identity token
* @return The resolved identity token
* @throws NullPointerException If the identity token is {@code null}
* @throws UnrecognizedIdentityTokenException If the identity token is not
* a valid identity token or it doesn't start with the expected prefix
*/
@NotNull
@IdentityTokenResolver
public static MyIdentity fromIdentityToken(
final @NotNull String token)
throws UnrecognizedIdentityTokenException {
final String body = IdentityTokenFormatter.parse(token);
try {
// Extract value from token body
final CustomType value;
if (body == null) {
value = null;
} else {
value = CustomType.fromString(body);
}
return new MyIdentity(value);
} catch (final SomeException se) {
throw new UnrecognizedIdentityTokenException(
"Unrecognized identity token", se);
}
}
}
Identities composed by multiple values can choice any bidirectional method to
format and parse the identity token body. The resulting String
will be
converted to Base64 to avoid illegal characters, so any suitable format is
allowed.
Abstract class AbstractComposedIdentity
provides a default implementation
that concatenates and splits the components with a custom separator (uses ,
by default):
class MyIdentity
extends AbstractComposedIdentity {
/** First identity component. */
private final CustomType value0;
/** Second identity component. */
private final CustomType2 value1;
/**
* Creates a new instance.
*
* @param value0 The first identity component
* @param value1 The second identity component
*/
public MyIdentity(
final CustomType value0,
final CustomType2 value1) {
super();
this.value0 = value0;
this.value1 = value1;
}
/**
* Returns the first identity component.
*
* @return The first identity component
*/
public CustomType getValue0() {
return this.value0;
}
/**
* Returns the second identity component.
*
* @return The second identity component
*/
public CustomType2 getValue1() {
return this.value1;
}
/**
* {@inheritDoc}
*/
@Override
protected @NotNull String[] getIdentityTokenBodyParts() {
return new String[] {
this.value0 == null ? null : this.value0.toString(),
this.value1 == null ? null : this.value1.toString(),
};
}
/**
* Resolves the specified identity token to a valid {@code MyIdentity}
*
* @param token The identity token
* @return The resolved identity token
* @throws NullPointerException If the identity token is {@code null}
* @throws UnrecognizedIdentityTokenException If the identity token is not
* a valid identity token or it doesn't start with the expected prefix
*/
@NotNull
@IdentityTokenResolver
public static MyIdentity fromIdentityToken(
final @NotNull String token)
throws UnrecognizedIdentityTokenException {
final String[] parts = AbstractComposedIdentity.extractRequiredTokenBodyParts(
token,
2);
try {
final CustomType value0;
if (parts[0] == null) {
value0 = null;
} else {
value0 = CustomType.fromString(parts[0]);
}
final CustomType2 value1;
if (parts[1] == null) {
value1 = null;
} else {
value1 = CustomType2.fromString(parts[1]);
}
return new MyIdentity(value0, value1);
} catch (final SomeException se) {
throw new UnrecognizedIdentityTokenException(
"Unrecognized identity token", se);
}
}
}
By default the default identity prefix provided by
IdentityTokenFormatter.DEFAULT_PREFIX
(an empty string) is used.
If a custom identity prefers to generate token with a custom prefix method
getIdentityTokenPrefix
can be overwritten, remembering to use same prefix
during token parsing:
class MyIdentity ... {
public static final String CUSTOM_PREFIX = "MYPREFIX";
/**
* {@inheritDoc}
*/
@Nonnull
@ValidIdentityTokenPrefix
@Override
protected String getIdentityTokenPrefix() {
return CUSTOM_PREFIX
}
/**
* Resolves the specified identity token to a valid {@code MyIdentity}
*
* @param token The identity token
* @return The resolved identity token
* @throws NullPointerException If the identity token is {@code null}
* @throws UnrecognizedIdentityTokenException If the identity token is not
* a valid identity token or it doesn't start with the expected prefix
*/
@NotNull
@IdentityTokenResolver
public static MyIdentity fromIdentityToken(
@NotNull
final String token)
throws UnrecognizedIdentityTokenException {
final String body = IdentityTokenFormatter.parse(
CUSTOM_PREFIX,
token);
// ...
}
}
Identity beans
For implementing bean classes with identities the interface IdentityBean
and
class BaseIdentityBean
are provided:
class MyBean extends BaseIdentityBean {
/**
* Empty constructor.
*/
public MyBean() {
super();
}
/**
* Copy constructor.
*
* @param copy The instance to copy
*/
public MyBean(
@Nonnull
final MyBean copy) {
super(copy);
// Copy properties
}
}
MyBean bean = new MyBean();
bean.setIdentity(beanIdentity);
assertEquals(
beanIdentity.getIdentityToken(),
bean.getIdentity().getIdentityToken());
Identity beans validation
For validating that a passed bean contains a valid identity annotation
ValidBeanIdentity
is provided. This annotation validates that a valid
identity is provided. If the identity is an instance of TokenIdentity
this
validation does not check that the token is in the expected format.
void myMethod(
@ValidBeanIdentity
MyBean bean) {
// ...
}
Utility class BeanValidationUtils
provides method isValidBeanIdentity()
to check programmatically if a IdentityBean
has a valid identity. Under the
hoods validates the bean against the IdentityBean.RequireIdentity
group.
Identity serialization and conversion
The library provides “out of the box” configuration for Java Serialization, Jackson based JSON serialization, JAXB based XML serialization and Java Beans editor based conversion.