I've created a class User that extends Document. User just has some simple constructors and getters/setters around some strings and ints. However, when I try to insert the User class into Mongo I get the following error:
Exception in thread "main" org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class com.foo.User.
at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46)
at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63)
at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:37)
at org.bson.BsonDocumentWrapper.asBsonDocument(BsonDocumentWrapper.java:62)
at com.mongodb.MongoCollectionImpl.documentToBsonDocument(MongoCollectionImpl.java:507)
at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:292)
at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:282)
at com.foo.bar.main(bar.java:27)
Sounds like I need to work with some Mongo Codecs stuff, but I'm not familiar with it and some quick googling returns some results that seem pretty advanced.
How do I properly write my User class for use in Mongo? Here is my class for reference:
public class User extends Document {
User(String user, List<Document > history, boolean isActive, String location){
this.append("_id", user)
.append("history", history)
.append("isActive", isActive)
.append("location", location);
}
public List<Document > getHistory(){
return this.get("history", ArrayList.class);
}
public void addToHistory(Document event){
List<Document> history = this.getHistory();
history.add(event);
this.append("history", history);
}
public boolean hasMet(User otherUser){
List<String> usersIveMet = this.getUsersMet(),
usersTheyMet = otherUser.getUsersMet();
return !Collections.disjoint(usersIveMet, usersTheyMet);
}
public List<String> getUsersMet() {
List<Document> usersHistory = this.getHistory();
List<String> usersMet = usersHistory.stream()
.map(doc -> Arrays.asList(doc.getString("user1"), doc.getString("user1")))
.filter(u -> !u.equals(this.getUser()))
.flatMap(u -> u.stream())
.collect(Collectors.toList());
return usersMet;
}
public String getUser(){
return this.getString("_id");
}
}
Since you are trying to create new object (even if you extend from Document), Mongo has no way to recognize it and therefore you need to provide encoding/decoding in order to let Mongo to know about your object (at least I cannot see other way than this..).
I played with your User class a bit and get it work.
So, here is how I defined a User class:
public class User {
private List<Document> history;
private String id;
private Boolean isActive;
private String location;
// Getters and setters. Omitted for brevity..
}
Then you need provide encoding/decoding logic to your User class:
public class UserCodec implements Codec<User> {
private CodecRegistry codecRegistry;
public UserCodec(CodecRegistry codecRegistry) {
this.codecRegistry = codecRegistry;
}
#Override
public User decode(BsonReader reader, DecoderContext decoderContext) {
reader.readStartDocument();
String id = reader.readString("id");
Boolean isActive = reader.readBoolean("isActive");
String location = reader.readString("location");
Codec<Document> historyCodec = codecRegistry.get(Document.class);
List<Document> history = new ArrayList<>();
reader.readStartArray();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
history.add(historyCodec.decode(reader, decoderContext));
}
reader.readEndArray();
reader.readEndDocument();
User user = new User();
user.setId(id);
user.setIsActive(isActive);
user.setLocation(location);
user.setHistory(history);
return user;
}
#Override
public void encode(BsonWriter writer, User user, EncoderContext encoderContext) {
writer.writeStartDocument();
writer.writeName("id");
writer.writeString(user.getId());
writer.writeName("isActive");
writer.writeBoolean(user.getIsActive());
writer.writeName("location");
writer.writeString(user.getLocation());
writer.writeStartArray("history");
for (Document document : user.getHistory()) {
Codec<Document> documentCodec = codecRegistry.get(Document.class);
encoderContext.encodeWithChildContext(documentCodec, writer, document);
}
writer.writeEndArray();
writer.writeEndDocument();
}
#Override
public Class<User> getEncoderClass() {
return User.class;
}
}
Then you need a codec provided for type checking before starting serialization/deserialization.
public class UserCodecProvider implements CodecProvider {
#Override
#SuppressWarnings("unchecked")
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
if (clazz == User.class) {
return (Codec<T>) new UserCodec(registry);
}
return null;
}
}
And finally, you need to register your provider to your MongoClient, that's all.
public class MongoDb {
private MongoDatabase db;
public MongoDb() {
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
CodecRegistries.fromProviders(new UserCodecProvider()),
MongoClient.getDefaultCodecRegistry());
MongoClientOptions options = MongoClientOptions.builder()
.codecRegistry(codecRegistry).build();
MongoClient mongoClient = new MongoClient(new ServerAddress(), options);
db = mongoClient.getDatabase("test");
}
public void addUser(User user) {
MongoCollection<User> collection = db.getCollection("user").withDocumentClass(User.class);
collection.insertOne(user);
}
public static void main(String[] args) {
MongoDb mongoDb = new MongoDb();
Document history1 = new Document();
history1.append("field1", "value1");
history1.append("field2", "value2");
history1.append("field3", "value3");
List<Document> history = new ArrayList<>();
history.add(history1);
User user = new User();
user.setId("someId1");
user.setIsActive(true);
user.setLocation("someLocation");
user.setHistory(history);
mongoDb.addUser(user);
}
}
A bit late but just stumbled across the issue and was also somewhat disappointed by the work involved in the proposed solutions so far. Especially since it requires tons of custom code for every single Document extending class you wish to persist and might also exhibit sub-optimal performance noticeable in large data sets.
Instead I figured one might piggyback off DocumentCodec like so (Mongo 3.x):
public class MyDocumentCodec<T extends Document> implements CollectibleCodec<T> {
private DocumentCodec _documentCodec;
private Class<T> _class;
private Constructor<T> _constructor;
public MyDocumentCodec(Class<T> class_) {
try {
_documentCodec = new DocumentCodec();
_class = class_;
_constructor = class_.getConstructor(Document.class);
} catch (Exception ex) {
throw new MCException(ex);
}
}
#Override
public void encode(BsonWriter writer, T value, EncoderContext encoderContext) {
_documentCodec.encode(writer, value, encoderContext);
}
#Override
public Class<T> getEncoderClass() {
return _class;
}
#Override
public T decode(BsonReader reader, DecoderContext decoderContext) {
try {
Document document = _documentCodec.decode(reader, decoderContext);
T result = _constructor.newInstance(document);
return result;
} catch (Exception ex) {
throw new MCException(ex);
}
}
#Override
public T generateIdIfAbsentFromDocument(T document) {
if (!documentHasId(document)) {
Document doc = _documentCodec.generateIdIfAbsentFromDocument(document);
document.put("_id", doc.get("_id"));
}
return document;
}
#Override
public boolean documentHasId(T document) {
return _documentCodec.documentHasId(document);
}
#Override
public BsonValue getDocumentId(T document) {
return _documentCodec.getDocumentId(document);
}
}
This is then registered along the lines of
MyDocumentCodec<MyClass> myCodec = new MyDocumentCodec<>(MyClass.class);
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(MongoClient.getDefaultCodecRegistry(),
CodecRegistries.fromCodecs(myCodec));
MongoClientOptions options = MongoClientOptions.builder().codecRegistry(codecRegistry).build();
MongoClient dbClient = new MongoClient(new ServerAddress(_dbServer, _dbPort), options);
Switching to this approach along with bulking up some operations (which probably has a large effect) I just managed to run an operation that previously took several hours to 30 mins. The decode method can probably be improved but my main concern was inserts for now.
Hope this helps someone. Please let me know if you see issues with this approach.
Thanks.
Have you tried using the #Embedded and #JsonIgnoreProperties(ignoreUnknown = true) on top of your class signature?
This worked for me when I had a similar issue. I had a model (Translation) which I was storing in a HashMap member field of another model (Promo).
Once I added these annotations to the Translation class signature, the issue went away. Not sure if it'll work that way in your case but worth trying.
I have to explore more on this myself.
Related
I have a question on the use of IO operations within java.util.function.Predicate. Please consider the following example:
public class ClientGroupFilter implements Predicate<Client> {
private GroupMapper mapper;
private List<String> validGroupNames = new ArrayList<>();
public ClientGroupFilter(GroupMapper mapper) {
this.mapper = mapper;
}
#Override
public boolean test(Client client) {
// this is a database call
Set<Integer> validsIds = mapper.getValidIdsForGroupNames(validGroupNames);
return client.getGroupIds().stream().anyMatch(validIds::contains);
}
public void permit(String name) {
validGroupNames.add(name);
}
}
As you can see this filter accepts any number of server group names, which are resolved by the mapper when a specific client is tested. If the client owns one of the valid server groups, true is returned.
Now, of course it is obivous that this is totally iniffecient if the filter is applied to multiple clients. So, refactoring lead me to this:
public class ClientGroupFilter implements Predicate<Client> {
private GroupMapper mapper;
private List<String> validGroupNames = new ArrayList<>();
private boolean updateRequired = true;
private Set<Integer> validIds = new HashSet<>();
public ClientGroupFilter(GroupMapper mapper) {
this.mapper = mapper;
}
#Override
public boolean test(Client client) {
if(updateRequired) {
// this is a database call
validIds = mapper.getValidIdsForGroupNames(validGroupNames);
updateRequired = false;
}
return client.getGroupIds().stream().anyMatch(validIds::contains);
}
public void permit(String name) {
validGroupNames.add(name);
updateRequired = true;
}
}
The performance is a lot better, of course, but im still not happy with the solution, since i feel like java.util.function.Predicate should not be used like this. However, i still want to be able to provide a fast solution to filter a list of clients, without the need to require the consumer to map the server group name to its ids.
Does anyone have a better idea to refactor this?
If your usage pattern is such that you call permit several times, and then use Predicate<Client> without calling permit again, you can separate the code that collects validGroupNames from the code of your predicate by using a builder:
class ClientGroupFilterBuilder {
private final GroupMapper mapper;
private List<String> validGroupNames = new ArrayList<>();
public ClientGroupFilter(GroupMapper mapper) {
this.mapper = mapper;
}
public void permit(String name) {
validGroupNames.add(name);
}
public Predicate<Client> build() {
final Set<Integer> validIds = mapper.getValidIdsForGroupNames(validGroupNames);
return new Predicate<Client>() {
#Override
public boolean test(Client client) {
return client.getGroupIds().stream().anyMatch(validIds::contains);
}
}
}
}
This restricts building of validIds to the point where we construct the Predicate<Client>. Once the predicate is constructed, no further input is necessary.
I am using #CascadeSave to save child object in separate collection.
My Document classes are :
public class FbUserProfile{
#Id
private long id;
#DBRef(lazy=true)
#CascadeSave()
private Set<FacebookFriend> friends;
#DBRef(lazy=true)
#CascadeSave()
private Set<FacebookFriendList> customFriendList;
}
public class FacebookFriend{
#Id
private long id;
private String name;
}
public class FacebookFriendList{
#Id
private long id;
private String name;
private String list_type;
}
I add some object in both friends,customFriendList.
and try to update fbUserProfile object using:
mongoTemplate.save(fbUserProfile);
note: fbUserProfile already exists in db. Now I am updating this
Error Message: Cannot perform cascade save on child object without id set
If I remove #CascadeSave. It works fine for me. How I can Cascade set objects.
I am also using #CascadeSave with other objects. Its working fine but they are not set object.
I found the same tutorials somewhere else: Baeldung's and JavaCodeGeeks (this is the one i've followed)
I've had that same problem, and I could solve it.
It happens when you try to persist a collection. It doesn't matter that the collection's items have the #Id, because the collection itself won't have it. I edited the code in the EventListener's onBeforeConvert to check if the field you're trying to CascadeSave is a collection (in my case a List). If it's a list, you just cycle through it checking each individual item for #Id and saving them.
If it's not a collection you still have to persist them the same way you did before
#Override
public void onBeforeConvert(Object source) {
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
#Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)){
final Object fieldValue = field.get(source);
if(fieldValue instanceof List<?>){
for (Object item : (List<?>)fieldValue){
checkNSave(item);
}
}else{
checkNSave(fieldValue);
}
}
}
});
}
private void checkNSave(Object fieldValue){
DbRefFieldCallback callback = new DbRefFieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
if (!callback.isIdFound()){
throw new MappingException("Oops, something went wrong. Child doesn't have #Id?");
}
mongoOperations.save(fieldValue);
}
The best way to set an ID on the dependent child object is to write a listener class by extending AbstractMongoEventListener class and override the onConvert() method.
public class CustomMongoEventListener extends
AbstractMongoEventListener<Object> {
#Autowired
private MongoOperations mongoOperations;
#Override
public void onBeforeConvert(final Object entity) {
if (entity.id == null || entity.id.isEmpty()) {
entity.id = generateGuid(); //generate random sequence ID
}
public static String generateGuid() {
SecureRandom randomGen = new SecureRandom();
byte[] byteArray = new byte[16];
randomGen.nextBytes(byteArray);
return new Base32().encodeToString(byteArray).substring(0,26);
}
}
Finally register your custom listener in `your configuration file. For annotation approach use the following code to register :
#Bean
public CustomMongoEventListener cascadingMongoEventListener() {
return new CustomMongoEventListener();
}
The above solution works fine incase if you have a list. But we can avoid firing a save query for each element from the list, as it reduces the performance. Here is the solution I have found out of the above code.
#Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
#Override
public void doWith(Field field)
throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)){
final Object fieldValue = field.get(source);
if(fieldValue instanceof List<?>){
for (Object item : (List<?>)fieldValue){
checkNAdd(item);
}
}else{
checkNAdd(fieldValue);
}
mongoOperations.insertAll(documents);
}
}
});
}
private void checkNAdd(Object fieldValue){
DbRefFieldCallback callback = new DbRefFieldCallback();
ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
if (!callback.isIdFound()){
throw new MappingException("Oops, something went wrong. Child doesn't have #Id?");
}
documents.add(fieldValue);
}
Okey I extend the class and it will check if the document is exist if it exist it will update the document else it insert the document:
#Component
class GenericCascadeMongo(
private val mongoTemplate: MongoTemplate
) : AbstractMongoEventListener<Any>() {
override fun onBeforeConvert(event: BeforeConvertEvent<Any?>) {
val source = event.source
?: return
ReflectionUtils.doWithFields(source.javaClass) { field ->
ReflectionUtils.makeAccessible(field)
if (field.isAnnotationPresent(DBRef::class.java) && field.isAnnotationPresent(CascadeSave::class.java)) {
val fieldValue = field[source]
?: return#doWithFields
if (fieldValue is List<*>) {
fieldValue.filterNotNull().forEach {
checkAndSave(it)
}
} else {
checkAndSave(fieldValue)
}
}
}
}
private fun checkAndSave(fieldValue: Any) {
try {
val callback = DbRefFieldCallback(fieldValue)
ReflectionUtils.doWithFields(fieldValue.javaClass, callback)
if (!callback.isIdFound && callback.id == null) {
mongoTemplate.insert(fieldValue)
}
if (callback.id != null) {
val findById = mongoTemplate.exists(Query(Criteria.where(MConst.MONGO_ID).`is`(callback.id)), fieldValue.javaClass)
if (findById) {
mongoTemplate.save(fieldValue)
} else {
mongoTemplate.insert(fieldValue)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private class DbRefFieldCallback(val fieldValue: Any?) : FieldCallback {
var isIdFound = false
private set
var id: String? = null
private set
#Throws(IllegalArgumentException::class, IllegalAccessException::class)
override fun doWith(field: Field) {
ReflectionUtils.makeAccessible(field)
if (field.isAnnotationPresent(Id::class.java)) {
isIdFound = true
id = ReflectionUtils.getField(field, fieldValue)?.toString()
}
}
}
}
I am trying to pull data from class in another class and populate a JPanel with the data, but it is not working for some reason.
Here is the full restConnector class where I pull the JSON data.
As far as I know this works fine.
public class restConnector {
private static final Logger LOGGER = LoggerFactory.getLogger(restConnector.class);
private static final restConnector INSTANCE = new restConnector();
public static restConnector getInstance() {
return restConnector.INSTANCE;
}
private restConnector(){
}
private static String user = "ss";
private static String pwd = "ee
public static String encode(String user, String pwd) {
final String credentials = user+":"+pwd;
BASE64Encoder encoder = new sun.misc.BASE64Encoder();
return encoder.encode(credentials.getBytes());
}
//Open REST connection
public static void init() {
restConnector.LOGGER.info("Starting REST connection...");
try {
Client client = Client.create();
client.addFilter(new LoggingFilter(System.out));
WebResource webResource = client.resource("https://somewebpage.com/
String url = "activepersonal";
ClientResponse response = webResource
.path("api/alerts/")
.queryParam("filter", ""+url)
.header("Authorization", "Basic "+encode(user, pwd))
.header("x-api-version", "1")
.accept("Application/json")
.get(ClientResponse.class);
if (response.getStatus() != 200) {
}else{
restConnector.LOGGER.info("REST connection STARTED.");
}
String output = response.getEntity(String.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new MyNameStrategy());
try {
List<Alert> alert = mapper.readValue(output, new TypeReference<List<Alert>>(){});
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() {
}
}
However, when I try to pull the data in another class it gives me just null values from the system.out.print inside refreshData() method. Here is the code that is supposed to print the data
public class Application{
Alert alerts = new Alert();
public Application() {
refreshData();
}
private void initComponents() {
restConnector.init();
refreshData();
}
private void refreshData() {
System.out.println("appalertList: "+alerts.getComponentAt(0));
}
}
Here is my Alert class
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(Include.NON_EMPTY)
public class Alert {
private int pasID;
private String status;
private boolean shared;
private String header;
private String desc;
public int getPasID() {
return pasID;
}
public void setPasID(int pasID) {
this.pasID = pasID;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public boolean isShared() {
return shared;
}
public void setShared(boolean shared) {
this.shared = shared;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\n***** Alert Details *****\n");
sb.append("PasID="+getPasID()+"\n");
sb.append("Status="+getStatus()+"\n");
sb.append("Shared="+isShared()+"\n");
sb.append("Header="+getHeader()+"\n");
sb.append("Description="+getDesc()+"\n");
sb.append("*****************************");
return sb.toString();
}
public String getComponentAt(int i) {
return toString();
}
}
I'm kind a lost with this and been stuck here for a couple of days already so all help would be really appreciated. Thanks for the help in advance.
Edit: Formatted the code a bit and removed the NullPointerException as it was not happening anymore.
As stated in comments:
Me: In your first bit of code you have this try { List<Alert> alert.., but you do absolutely nothing with the newly declared alert List<Alert>. It this where the data is supposed to be coming from?
OP: I'm under the impression that that bit of code is the one that pushes the JSON Array to the Alert.class. Is there something I'm missing there?
Me: And what makes you think it does that? All it does is read the json, and the Alert.class argument is the class type argument, so the mapper know the results should be mapped to the Alert attributes when it creates the Alert objects. That's how doing List<Alert> is possible, because passing Alert.class decribes T in List<T>. The List<Alert> is what's returned from the reading, but you have to determine what to actually do with the list. And currently, you do absolutely nothing with it
You maybe want to change the class just a bit.
Now this is in no way a good design, just an example of how you can get it to work. I would take some time to sit and think about how you want the restConnector to be fully utilized
That being said, you can have a List<Alert> alerts; class member in the restConnector class. And have a getter for it
public class restConnector {
private List<Alert> alerts;
public List<Alert> getAlerts() {
return alerts;
}
...
}
Then when deserializing with the mapper, assign the value to private List<Alert> alerts. What you are doing is declaring a new locally scoped list. So instead of
try {
List<Alert> alert = mapper.readValue...
do this instead
try {
alerts = mapper.readValue
Now the class member is assigned a value. So in the Application class you can do something like
public class Application {
List<Alert> alerts;
restConnector connect;
public Application() {
initComponents();
}
private void initComponents() {
connector = restConnector.getInstance();
connector.init();
alerts = connector.getAlerts();
refreshData();
}
private void refreshData() {
StringBuilder sb = new StringBuilder();
for (Alert alert : alerts) {
sb.append(alert.toString()).append("\n");
}
System.out.println("appalertList: "+ sb.toString());
}
}
Now you have access to the Alerts in the list.
But let me reiterate: THIS IS A HORRIBLE DESIGN. For one you are limiting the init method to one single call, in which it is only able to obtain one and only one resource. What if the rest service needs to access a different resource? You have made the request set in stone, so you cant.
Take some time to think of some good OOP designs where the class can be used for different scenarios.
currently I have got this class which implements the Builder pattern, for sake of readibility I have chosen to omit some methods, more precisely I only show the build methods of username.
package dao.constraint;
import java.util.Arrays;
public class AccountConstraint {
private Constraint<Range<Integer>> accountIdConstraint;
private Constraint<String> usernameConstraint;
private Constraint<String> passwordConstraint;
private Constraint<String> emailConstraint;
private AccountConstraint(Builder builder) {
this.accountIdConstraint = builder.accountIdConstraint;
this.usernameConstraint = builder.usernameConstraint;
this.passwordConstraint = builder.passwordConstraint;
this.emailConstraint = builder.emailConstraint;
}
public Constraint<Range<Integer>> getAccountIdConstraint() {
return accountIdConstraint;
}
public Constraint<String> getUsernameConstraint() {
return usernameConstraint;
}
public Constraint<String> getPasswordConstraint() {
return passwordConstraint;
}
public Constraint<String> getEmailConstraint() {
return emailConstraint;
}
public Constraint[] getConstraints() {
return Arrays.asList(this.getAccountIdConstraint(), this.getUsernameConstraint(), this.getPasswordConstraint(), this.getEmailConstraint()).toArray(new Constraint[4]);
}
public static class Builder {
private Constraint<Range<Integer>> accountIdConstraint;
private Constraint<String> usernameConstraint;
private Constraint<String> passwordConstraint;
private Constraint<String> emailConstraint;
public Builder() {
this.accountIdConstraint = null;
this.usernameConstraint = null;
this.passwordConstraint = null;
this.emailConstraint = null;
}
public Builder username(final String username) {
this.usernameConstraint = new Constraint<>(Operation.IS, true, username, "username");
return this;
}
public Builder notUsername(final String username) {
this.usernameConstraint = new Constraint<>(Operation.IS, false, username, "username");
return this;
}
public Builder usernameLike(final String username) {
this.usernameConstraint = new Constraint<>(Operation.LIKE, true, username, "username");
return this;
}
public Builder usernameNotLike(final String username) {
this.usernameConstraint = new Constraint<>(Operation.LIKE, false, username, "username");
return this;
}
public AccountConstraint build() {
return new AccountConstraint(this);
}
}
}
As you can see there is very subtle difference between AccountConstraint.Builder.username(String s) and AccountConstraint.Builder.notUsername(String s).
I would like to be able to write something like new AccountConstraint.Builder().not(username(s));. However as I know this is not valid Java syntax if username(String s) is not defined in the calling Java class. I neither wish to repeat the whole AccountConstraint.Builder() again to reach the username(String s) part. Any solutions?
Second question: Can AccountConstraint.getConstraints() be improved or written more simple?
Regards.
you could make not a method of your builder, setting a flag, which then negates the next constraint.
private boolean negate = false;
public Builder not() {
negate = true;
}
public Builder username(final String username) {
this.usernameConstraint = new Constraint<>(Operation.IS, !negate, username, "username");
negate = false;
return this;
}
For your second question:
public Constraint[] getConstraints() {
return Arrays.asList(this.getAccountIdConstraint(),
this.getUsernameConstraint(),
this.getPasswordConstraint(),
this.getEmailConstraint())
.toArray(new Constraint[4]);
}
can be re-written to :
public Constraint[] getConstraints() {
return new Constraint[] {
this.accountIdConstraint,
this.usernameConstraint,
this.passwordConstraint,
this.emailConstraint
};
}
But IMO, returning a List or Set would be better than an array.
What I find extremely elegant in this situations is to write a utility class with static factory methods like.
public static Constraint userName(...) { ... }
and to import static blabla.Utility.username;
Then you can write almost declarative human-readable queries in java. This is very much as for the hamcrest library for unit testing where you write something like.
Assert.assertThat(blabla, is(equalTo(nullValue()));
In this case Not should implement Constraint and just negates the nested (referenced) constraint like this:
public static Constraint not(Constraint negated) { return new Not(negated); }
this results in code like
PreparedStatement ps = new QueryBuilder()
.select()
.from(table("accounts")
.where(not(username(equalTo("blabla")))
.compile();
You can add static factories for boolean combinations:
.where(and(
.not(...),
.not(or(...))
Defining constraints like this (static factory methods as opposed to adding them to the builder) thus makes them easily composable.
Could you guys please help me find where I made a mistake ?
I switched from SimpleBeanEditorDriver to RequestFactoryEditorDriver and my code no longer saves full graph even though with() method is called. But it correctly loads full graph in the constructor.
Could it be caused by circular reference between OrganizationProxy and PersonProxy ? I don't know what else to think :( It worked with SimpleBeanEditorDriver though.
Below is my client code. Let me know if you want me to add sources of proxies to this question (or you can see them here).
public class NewOrderView extends Composite
{
interface Binder extends UiBinder<Widget, NewOrderView> {}
private static Binder uiBinder = GWT.create(Binder.class);
interface Driver extends RequestFactoryEditorDriver<OrganizationProxy, OrganizationEditor> {}
Driver driver = GWT.create(Driver.class);
#UiField
Button save;
#UiField
OrganizationEditor orgEditor;
AdminRequestFactory requestFactory;
AdminRequestFactory.OrderRequestContext requestContext;
OrganizationProxy organization;
public NewOrderView()
{
initWidget(uiBinder.createAndBindUi(this));
requestFactory = createFactory();
requestContext = requestFactory.contextOrder();
driver.initialize(requestFactory, orgEditor);
String[] paths = driver.getPaths();
createFactory().contextOrder().findOrganizationById(1).with(paths).fire(new Receiver<OrganizationProxy>()
{
#Override
public void onSuccess(OrganizationProxy response)
{
if (response == null)
{
organization = requestContext.create(OrganizationProxy.class);
organization.setContactPerson(requestContext.create(PersonProxy.class));
} else
organization = requestContext.edit(response);
driver.edit(organization, requestContext);
}
#Override
public void onFailure(ServerFailure error)
{
createConfirmationDialogBox(error.getMessage()).center();
}
});
}
private static AdminRequestFactory createFactory()
{
AdminRequestFactory factory = GWT.create(AdminRequestFactory.class);
factory.initialize(new SimpleEventBus());
return factory;
}
#UiHandler("save")
void buttonClick(ClickEvent e)
{
e.stopPropagation();
save.setEnabled(false);
try
{
AdminRequestFactory.OrderRequestContext ctx = (AdminRequestFactory.OrderRequestContext) driver.flush();
if (!driver.hasErrors())
{
// Link to each other
PersonProxy contactPerson = organization.getContactPerson();
contactPerson.setOrganization(organization);
String[] paths = driver.getPaths();
ctx.saveOrganization(organization).with(paths).fire(new Receiver<Void>()
{
#Override
public void onSuccess(Void arg0)
{
createConfirmationDialogBox("Saved!").center();
}
#Override
public void onFailure(ServerFailure error)
{
createConfirmationDialogBox(error.getMessage()).center();
}
});
}
} finally
{
save.setEnabled(true);
}
}
}
with() is only used for retrieval of information, so your with() use with a void return type is useless (but harmless).
Whether a full graph is persisted is entirely up to your server-side code, which is intimately bound to your persistence API (JPA, JDO, etc.)
First, check that the Organization object you receive in your save() method on the server-side is correctly populated. If it's not the case, check your Locators (and/or static findXxx methods) ; otherwise, check your save() method's code.
Judging from the code above, I can't see a reason why it wouldn't work.
It took me some time to realize that the problem was the composite id of Person entity.
Below is the code snippet of PojoLocator that is used by my proxy entities.
public class PojoLocator extends Locator<DatastoreObject, Long>
{
#Override
public DatastoreObject find(Class<? extends DatastoreObject> clazz, Long id)
{
}
#Override
public Long getId(DatastoreObject domainObject)
{
}
}
In order to fetch child entity from DataStore you need to have id of a parent class. In order to achieve that I switched "ID class" for Locator<> to String which represents textual form of Objectify's Key<> class.
Here is how to looks now:
public class PojoLocator extends Locator<DatastoreObject, String>
{
#Override
public DatastoreObject find(Class<? extends DatastoreObject> clazz, String id)
{
Key<DatastoreObject> key = Key.create(id);
return ofy.load(key);
}
#Override
public String getId(DatastoreObject domainObject)
{
if (domainObject.getId() != null)
{
Key<DatastoreObject> key = ofy.fact().getKey(domainObject);
return key.getString();
} else
return null;
}
}
Please note that your implementation may slightly differ because I'm using Objectify4.