I am trying to collect some objects in Drools, but I want to only collect objects which have the same attribute. To wit, imagine a TestData class:
public class TestData {
int number;
String name;
//Getters, setters, rest of class
}
I want to use collect to get all TestDatas which have the same name. For the following data set, I want a rule which can collect the first two (both having the name 'Test1') and the second two ('Test2') as separate collections.
custom.package.TestData< number: 1, name:'Test1' >
custom.package.TestData< number: 2, name:'Test1' >
custom.package.TestData< number: 3, name:'Test2' >
custom.package.TestData< number: 4, name:'Test2' >
Something like
rule "Test Rule"
when
$matchingTestDatas : ArrayList( size > 1 ) from collect ( TestData( *magic* ) )
then
//Code
end
But obviously that won't work without the magic- it gets me an array of all the TestData entries with every name. I can use that as the basis for a rule, and do extended processing in the right hand side iterating over all the test data entries, but it feels like there should be something in drools which is smart enough to match this way.
Presumably the "magic" is just:
TestData( name == 'Test1' )
or
TestData( name == 'Test2' )
... for each of the collections. But that seems too obvious. Am I missing something?
Based on the clarification from the OP in the comments on this answer, it would appear that a Map of collections is required, keyed from the name. To support this, accumulate is required, rather than collect.
$tests: HashMap()
from accumulate( $test : TestData( name == $name ),
init( HashMap tests = new HashMap(); ),
action(
if (tests.get($name) == null) tests.put($name, new ArrayList();
tests.get($name).add($test);
),
reverse(
tests.get($name).remove($test);
),
result( tests )
);
Related
I have got below structure in Java:
public class Request {
List<Product> product;
List<Account> accounts;
}
public class Product {
String productIdOne;
String productIdTwo;
String productTax;
}
public class Account {
List<ProductRelationship> productsRelationship;
}
public class ProductRelationship {
String productIdOne;
String productIdTwo;
}
And the request is the fact object send to drools. I am wondering how I can check if there is at least one product that productTax is set to 'true' and there is a relationship between one account and one product. In other words, if there is a product with tax set to true and at least one account contains a relationship with this product (by productIdOne and productIdTwo) then the rule result should pass;
The main issue is that the list of the product relationship is inside the account list.
Thanks for any advice
You have a rather straight-forward set of conditions, so it is possible to write a relatively simple rule to check them. I will consider each condition separately and then combine them into a final rule.
As you wrote:
there is at least one product that productTax is set to 'true'
Now, as you mentioned, your rule inputs are the Request instance which contains two lists (products, accounts.) We'll start by declaring that:
rule "Account exists with taxed product"
when
Request( $products: product != null,
$accounts: accounts != null )
Next, we want to find the taxed product. If we only wanted to prove the existence of the product, we could use an exists( ... ) condition, which is extremely fast. However since we want to do further comparisons, we'll want to actuall find the product with this condition and save a reference to it.
$taxedProduct: Product( productTax == "true" ) from $products
I've assumed here that any value other than exactly "true" is indicative of an untaxed product. You should adjust as needed (and possibly consider changing this type to a boolean.)
The next condition is to find the account:
there is a relationship between one account and [the taxed] product
First, we'll need to update our $taxedProduct declaration and get references to its ids:
$taxedProduct: Product( productTax == "true",
$id1: productIdOne,
$id2: productIdTwo ) from $products
Now we need to find an account with a matching relationship.
$account: Account( $relationships: productsRelationship != null ) from $accounts
exists( ProductRelationship( productIdOne == $id1,
productIdTwo == $id2 ) from $relationships )
Here, I used an exists condition for the relationship because we don't need to refer to the relationship itself ever again, just verify that the relationship exists. I did declare a variable $account to point to the account that has the product relationship.
Putting it all together, we have:
rule "Account exists with taxed product"
when
Request( $products: product != null,
$accounts: accounts != null )
$taxedProduct: Product( productTax == "true",
$id1: productIdOne,
$id2: productIdTwo ) from $products
$account: Account( $relationships: productsRelationship != null ) from $accounts
exists( ProductRelationship( productIdOne == $id1,
productIdTwo == $id2 ) from $relationships )
then
// We have a taxed product $taxedProduct
// and an associated account $account
end
When I first started with Drools I found it difficult to wrap my head around the way it treated objects in lists, which is why the ProductRelationship sub-list seems like a tricky issue on its face. What Drools is going to do is iterate through the $products list and find those Product instances that meet our criteria (namely, have productTax == "true".) Once it has found these taxed products, it then similarly goes through the $accounts list and finds all Accounts that meet the criteria (which have a productsRelationship list.) Then for each of those accounts, it is going to test that there exists a relationship as we've defined.
This is a simplified explanation, of course, but it helps to form a mental model of roughly what Drools is doing here. In reality Drools is much more efficient than then roughly O(n^3) workflow I've just described.
An interesting thing you should keep in mind is that this rule is not going to "stop" as soon as it finds a match. If you have two taxed products that have a relationship to a single account, this rule will fire twice -- once for each taxed product. Or, alternatively, if you have one taxed product and two accounts that have a relationship to it, the rule will fire twice (once for each account.) Basically, the rule will fire once for each "match" it finds in the given request.
can you try this following
rule "sample"
no-loop
when
request:Request(accountList: accounts)
request1:Request(productList: product)
Account(productsRelationshipList:ProductRelationship) from accountList
Product(productId contains productsRelationshipList, productTax = true ) from productList
then
System.out.println("Rule fired satisfied");
end
I am solving a problem similar to employee rostering. I have an additional constraint. The employees have a "type" value assigned to them. It's a hard constraint that atleast 1 employee of each "type" be there everyday. I have modelled it as follows:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
not Shift(employeeId != null, $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
This rule however, does not consider that the constraint be satisfied on each day. I have a list of date strings. How can I iterate over them in the drools file in the same manner that I am on the EmployeeType enum?
Edit: I figured out a way but it feels like a hack. When initialising the list of date strings, I also assign it to a static variable. Then I am able to use the static variable similar to the enum.
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
$date: String() from Constants.dateStringList;
not Shift(employeeId != null, $date == getDate(), $employee: getEmployee(), $employee.getType() == $type.getValue())
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
Don't think this is the correct approach though.
Your approach works, but having to define dynamic configurations in a static property of a class doesn't sound right (like you pointed out).
One solution would be to either use a global in the session, or to have a fact class that specify this configuration.
Using a global
If you decide to take this approach, then you need to define a global of type List<String> in your DRL and then use it in your rules in combination with the memberOf operator:
global List<String> dates;
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
not Shift(
employeeId != null,
date memberOf dates,
$employee: getEmployee(),
$employee.getType() == $type.getValue()
)
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
It is recommended to set the value for global before you insert any fact Shift into you session:
List<String> dates = //get the List from somewhere
ksession.setGlobal("dates", dates);
Using a Fact Class
Other than a global, you can model your configuration as a class. This makes things easier if you want for example to modify the configuration inside the rules themselves.
for this approach you will need to have a class containing the List<String> first. You could in theory insert the List<String> without wrapping it in any class, but this will make things hard to read and maintain.
public class DatesConfiguration {
private List<String> dates;
//... getters + setters
}
Then, you need to instantiate an object of this class and to insert it into your session:
DatesConfiguration dc = new DatesConfiguration();
dc.setDates(...);
ksession.insert(dc);
At this point, the object you have created is just another fact for Drools and can be used in your rules:
rule "All employee types must be covered"
when
$type: Constants.EmployeeType() from Constants.EmployeeType.values()
DatesConfiguration($dates: dates)
not Shift(
employeeId != null,
date memberOf $dates,
$employee: getEmployee(),
$employee.getType() == $type.getValue()
)
then
scoreHolder.addHardConstraintMatch(kcontext, -100);
end
Hope it helps,
I want to know how to modify a object in a list . I tried following, but it gives a error.
when
Category( $bookList : books )
UserProfile( profile == UserProfile.STUDENT )
$book : Book( student == true )
$category : Category( books contains $group )
then
modify( $category.books[$book] ) { setEligible(true) }
end
Book.setEligible is the method i need to call. But i need to call this for selected object in Cagegory.books list. What am i doing wrong ? can anyone help ?
Thanks !
First thing: Are you sure you want to include 2 different Category patterns in your rule? If you have 2 different categories, you may end up with 4 executions of that rule.
Second thing, if you want to modify the book, why don't you just do:
modify($book) {
setEligible(true)
}
Hope it helps,
You also need to make sure that Book is a fact.. that means that you are inserting that Fact into the Ksession..
What is the error that you are getting? which version of drools are you using?
I am modifying an item in a nested collection like this...
[using "from"]
rule "4G complete"
salience -1
when
$tr: TopTowerResult()
$ptncascade: PtnCascade() from $tr.cascadeList
Timestamp() from $ptncascade.cascadeFact.actual4g
then
$ptncascade.getCascadeFact().setComplete4g(true);
$ptncascade.getCascadeFact().setEstimate4g("Completed");
end
Explanation -
This line matches every instance of PtnCascade in $tr.cascadeList
You could add an additional pattern here to limit the selected items from the list.
$ptncascade: PtnCascade() from $tr.cascadeList
The next line operates on every instance of PtnCascade() that was matched.
Timestamp() from $ptncascade.cascadeFact.actual4g
The consequence also operates on the specific PtnCascade that was matched in the list -
$ptncascade.getCascadeFact().setComplete4g(true);
I pass my template a TreeSet with Strings. However, when I loop over the set like this:
#(usernames : TreeSet[String])
#for( name <- usernames){
#name ,
}
However, the names are never printed in the correct order.
How can I iterate over my set in my template and print the names in order?
This has something to do with the way Scala Templates work. I suspect your TreeSet collection is under the hood mapped to a different collection and as a result the ordering is not preserved.
There is clearly a difference between the behavior of the Scala for loop and the for loop in Scala Templates. If you run your code as regular Scala code the order of the TreeSet is obviously preserved:
val users = TreeSet("foo", "bar", "zzz", "abc")
for (user <- users) {
println(user)
}
One of the ways to solve the problem is to use the iterator in the Scala Template:
#for(name <- usernames.iterator) {
#name ,
}
or transform the TreeSet to a sequence:
#for(name <- usernames.toSeq) {
#name ,
}
There is no guaranteed ordering for any Set class, so it's best to sort it before iterating.
If you mean to print them alphabetically, you should convert it into a List and then iterate
#(usernames : TreeSet[String])
#for( name <- usernames.toList().sortWith(_ < _)){
#name ,
}
I'm trying to execute a query from java against a Map/Reduce view I have created on the CouchDB.
My map function looks like the following:
function(doc) {
if(doc.type == 'SPECIFIC_DOC_TYPE_NAME' && doc.userID){
for(var g in doc.groupList){
emit([doc.userID,doc.groupList[g].name],1);
}
}
}
and Reduce function:
function (key, values, rereduce) {
return sum(values);
}
The view seems to be working when executed from the Futon interface (without keys specified though).
What I'm trying to do is to count number of some doc types belonging to a single group. I want to query that view using 'userID' and name of the group as a keys.
I'm using Ektorp library for managing CouchDB data, if I execute this query without keys it returns the scalar value, otherwise it just prints an error saying that for reduce query group=true must be specified.
I have tried the following:
ViewQuery query = createQuery("some_doc_name");
List<String> keys = new ArrayList<String>();
keys.add(grupaName);
keys.add(uzytkownikID);
query.group(true);
query.groupLevel(2);
query.dbPath(db.path());
query.designDocId(stdDesignDocumentId);
query.keys(keys);
ViewResult r = db.queryView(query);
return r.getRows().get(0).getValueAsInt();
above example works without 'keys' specified.
I have other queries working with ComplexKey like eg:
ComplexKey key = ComplexKey.of(userID);
return queryView("list_by_userID",key);
but this returns only a list of type T (List) - using CouchDbRepositorySupport of course - and cannot be used with reduce type queries (from what I know).
Is there any way to execute the query with reduce function specified and a complex key with 2 or more values using Ektorp library? Any examples highly appreciated.
Ok, I've found the solution using trial and error approach:
public int getNumberOfDocsAssigned(String userID, String groupName) {
ViewQuery query = createQuery("list_by_userID")
.group(true)
.dbPath(db.path())
.designDocId(stdDesignDocumentId)
.key(new String[]{userID,groupName});
ViewResult r = db.queryView(query);
return r.getRows().get(0).getValueAsInt();
}
So, the point is to send the complex key (not keys) actually as a single (but complex) key containing the String array, for some reason method '.keys(...)' didn't work for me (it takes a Collection as an argument). (for explanation on difference between .key() and .keys() see Hendy's answer)
This method counts all documents assigned to the specific user (specified by 'userID') and specific group (specified by 'groupName').
Hope that helps anybody executing map/reduce queries for retrieving scalar values from CouchDB using Ektorp query.
Addition to Kris's answer:
Note that ViewQuery.keys() is used when you want to query for documents matching a set of keys, not for finding document(s) with a complex key.
Like Kris's answer, the following samples will get document(s) matching the specified key (not "keys")
viewQuery.key("hello"); // simple key
viewQuery.key(documentSlug); // simple key
viewQuery.key(new String[] { userID, groupName }); // complex key, using array
viewQuery.key(ComplexKey.of(userID, groupName)); // complex key, using ComplexKey
The following samples, on the other hand, will get document(s) matching the specified keys, where each key may be either a simple key or a complex key:
// simple key: in essence, same as using .key()
viewQuery.keys(ImmutableSet.of("hello"));
viewQuery.keys(ImmutableSet.of(documentSlug1));
// simple keys
viewQuery.keys(ImmutableSet.of("hello", "world"));
viewQuery.keys(ImmutableSet.of(documentSlug1, documentSlug2));
// complex key: in essence, same as using .key()
viewQuery.keys(ImmutableSet.of(
new String[] { "hello", "world" } ));
viewQuery.keys(ImmutableSet.of(
new String[] { userID1, groupName1 } ));
// complex keys
viewQuery.keys(ImmutableSet.of(
new String[] { "hello", "world" },
new String[] { "Mary", "Jane" } ));
viewQuery.keys(ImmutableSet.of(
new String[] { userID1, groupName1 },
new String[] { userID2, groupName2 } ));
// a simple key and a complex key. while technically possible,
// I don't think anybody actually does this
viewQuery.keys(ImmutableSet.of(
"hello",
new String[] { "Mary", "Jane" } ));
Note: ImmutableSet.of() is from guava library.
new Object[] { ... } seems to have same behavior as ComplexKey.of( ... )
Also, there are startKey() and endKey() for querying using partial key.
To send an empty object {}, use ComplexKey.emptyObject(). (only useful for partial key querying)