It combines interface based proxies with java.util.Map backends. This means that you can define typesafe handlers to
dynamic structures as Map and Collection of maps over the Map and Collection defined generics. It implements MapHolder
interface
also.
public interface MapHolder {
Map<String, Object> toMap();
Map<String, Object> getOriginalMap();
<T> T adaptTo(Class<T> clazz);
}
It can handle nested interfaces too, so all types of structures can be defined with a structure of interfaces.
The map entries and interface methods are mapped as per the JavaBean standard specification. This results in for example:
a String named as stringValue
in a Map to have a corresponding interface containing getStringValue
and setStringValue
.
It also can be used to transform values from/to java beans. The MapHolder
contains methods which
can convert to map or adapt to beans. When the interface contains the given method the casting to the MapHolder
not needed, the method can be called on the given interface directly.
Add the following to your pom.xml:
<dependency>
<groupId>hu.blackbelt</groupId>
<artifactId>structured-map-proxy</artifactId>
<version>LATEST</version>
<type>bundle</type>
</dependency>
In this example we have a User
which can have a UserDetail
as a single and containment collection.
public interface Identifier extends Serializable {
@Key(name = "__id")
Serializable getId();
void setId(Serializable id);
@Key(name = "__type")
String getType();
void setType(String id);
}
public interface Entity extends Serializable {
@Embedded
Identifier getId();
void setId(Identifier id);
@Embedded
Identifier identifier();
}
public interface User extends Entity {
Boolean getActive();
void setActive(Boolean active);
String getCredential();
void setCredential(String credential);
String getEmail();
void setEmail(String email);
Optional<String> getFirstName();
void setFirstName(String firstName);
@Key(name = "last_name")
Optional<String> getLastName();
void setLastName(String lastName);
Optional<String> getLoginName();
void setLoginName(String loginName);
LocalDateTime getLastLoginTime();
void setLastLoginTime(LocalDateTime lastLoginTime);
UserDetail getSingleUserDetail();
void setSingleUserDetail(UserDetail userDetail);
Collection<UserDetail> getUserDetails();
void setUserDetails(Collection<UserDetail> userDetails);
}
public interface UserDetail {
@Embedded
Identifier getId();
void setId(Identifier id);
String getNote();
void setNote(String note);
}
// Create map represents fields.
Map<String, Object> prepared = new HashMap<>();
prepared.put("active", true);
prepared.put("__id", "1");
prepared.put("__type", "user");
prepared.put("email", Optional.of("[email protected]"));
prepared.put("loginName", Optional.of("teszt"));
prepared.put("lastLoginTime", Optional.of(time));
prepared.put("last_name", Optional.of("teszt"));
prepared.put("userDetails", ImmutableList.of(ImmutableMap.of("__id", "1", "__type", "UserDetail", "note", "Note1")));
User user = MapProxy.builder(User.class)
.withMap(prepared)
.newInstance();
user.setEmail("[email protected]");
// Create an empty proxy
User user = MapProxy.builder(User.class).newInstance();
user.setActive(true);
user.setLoginName("teszt");
user.setId(MapProxy.builder(Identifier.class).id("1").type("User").newInstance());
UserDetail userDetail1 = MapProxy.builder(UserDetail.class).newInstance();
userDetail1.setId(MapProxy.builder(Identifier.class).id("1").type("UserDetail").newInstance());
userDetail1.setNote("Note1");
UserDetail userDetail2 = MapProxy.builder(UserDetail.class).newInstance();
userDetail2.setId(MapProxy.builder(Identifier.class).id("2").type("UserDetail").newInstance());
userDetail2.setNote("Note2");
user.setUserDetails(ImmutableList.of(userDetail1, userDetail2));
Map<String, Object> mapRepresentation = ((MapHolder) user).toMap();
Not only Bean-type proxies are supported. Interfaces for builders can also be defined
public interface UserBuilder {
UserBuilder id(Serializable id);
UserBuilder active(Boolean par);
UserBuilder credential(String par);
UserBuilder email(String par);
UserBuilder firstName(String par);
UserBuilder lastName(String par);
UserBuilder loginName(String par);
UserBuilder lastLoginTime(LocalDateTime par);
UserBuilder userDetails(Collection<UserDetail> userDetails);
UserBuilder singleUserDetail(UserDetail userDetail);
User build();
}
User user = MapBuilderProxy.builder(UserBuilder.class, User.class).newInstance()
.id(MapProxy.builder(Identifier.class).id("1").type("User").newInstance())
.active(true)
.loginName("teszt")
.build();
Define standard java beans with setters and getters.
@Builder
@Getter
@Setter
public class IdentifierBean {
Serializable id;
String type;
}
@Builder(builderMethodName = "entityBeanBuilder")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class EntityBean {
IdentifierBean compositeIdentifier;
Serializable id;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserBean extends EntityBean {
String email;
UserDetailBean singleUserDetail;
Collection<UserDetailBean> userDetails;
@Builder(builderMethodName = "userBeanBuilder")
public UserBean(IdentifierBean compositeIdentifier,
Serializable id,
String email,
UserDetailBean singleUserDetail,
Collection<UserDetailBean> userDetails
) {
super(compositeIdentifier);
this.email = email;
this.singleUserDetail = singleUserDetail;
this.userDetails = userDetails;
}
}
@Builder
@Getter
@Setter
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailBean {
String id;
String note;
}
User user = MapBuilderProxy.builder(UserBuilder.class, User.class).newInstance()
.id(MapProxy.builder(Identifier.class).id("1").type("User").newInstance())
.active(true)
.email("teszt")
.build();
UserBean bean = user.adaptTo(UserBean.class);
You can define static hashCode
method on the interface. On that case that method will be
performed on the proxy’s object call. If not defined the toString
method’s hashCode
is
called.
public interface A {
String getFld();
static int hashCode(A o) {
o.getFld().hashCode();
}
}
You can define static toString
method on the interface. On that case that method will be
performed on the proxy’s object call. If not defined the fields are iterated and printed.
public interface UserDetail {
@Key(name = "__id")
String getId();
void setId(String id);
String getNote();
void setNote(String note);
static String toString(UserDetail o1) {
return String.format("{ id: %s, note: %s }",
Objects.toString(o1.getId(), "null"),
Objects.toString(o1.getNote(), "null")
);
}
}
You can define static equals
method on the interface. On that case that method will be
performed on the proxy’s object call. If not defined the toString
method’s equals
is
called.
public interface UserDetail {
@Key(name = "__id")
String getId();
void setId(String id);
String getNote();
void setNote(String note);
static boolean equals(UserDetail o1, Object o2) {
if (o2 == null) {
return false;
}
if (UserDetail.class.isAssignableFrom(o2.getClass())) {
return o1.getId().equals(((UserDetail) o2).getId());
}
return false;
}
}
MapProxy.builder(User.class).withImmutable(true).newInstance();
MapProxy supports the following options:
-
immutable (default
false
) Adjusts the fields and collections in the created proxy as immutable, which results in all mutating operation calls triggering anIllegalStateException
-
nullSafeCollection (default
false
) If set totrue
and given a collection is not set in the map, an empty collection will be returned. Which means that the collection in the interface can never be null. -
enumMappingMethod (default
name
) When an enum type is defined as a field, we need to define what type of representation is coded in the map. -
mapNullToOptionalAbsent (default
false
) When set to true and a value is not present, the proxy will return with anOptional
value where the.isPresent()
check will result in false.
The MapBuilderProxy’s builder supports all of the options of MapProxy with a couple of addition. For example:
User user = MapBuilderProxy.builder(UserBuilder.class, User.class).withBuilderMethodPrefix("with").newInstance()
.id("1")
.active(true)
.loginName("teszt")
.build();
-
builderMethodPrefix (default
false
) It defines whether the buidler method can have a prefix or not. By default the builder method names match with the field name. With this option you can rename them. -
enumMappingMethod (default
name
) When an enum type is defined as a field, we need to define what type of representation is coded in the map.
There are annotations which helps to configure the mapping between Map and the interface. The annotations have to be defined in the getter method.
Sometimes we need to store information in a hierarchical structure with or without
dedicated interfaces. For such cases we can use the @Embedded
annotation.
The @Embedded
annotation can be used on methods which are not getters for
factory like functions.
public interface Identifier extends Serializable {
@Key(name = "__id")
Serializable getId();
void setId(Serializable id);
@Key(name = "__type")
String getType();
void setType(String id);
}
public interface Entity extends Serializable {
@Embedded
Identifier getId();
void setId(Identifier id);
String getName();
void setName(String name);
}
In this example the Entity’s map will contain type
and id
fields with name
.
So in the Map it is flattened, bu8t from MapProxy it can be accessed as an embedded
MapProxy.
For better performance some fields and methods are cached. The cache eviction time
can be set as system property with -D
.
structuredMapProxyCacheExpireInSecond
is 60
by default.
Everyone is welcome to contribute to structured-map-proxy! As a starter, please read the corresponding CONTRIBUTING guide for details!
This project is licensed under the Apache License 2.0.