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("test@test.com"));
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("another@example.com");// 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 totrueand 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 anOptionalvalue 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.