I am using transaction event handlers to do security checking on deleted nodes, to make sure whether a current user is allowed to do this.
To make sure I have the right node to check I first have to find out whether it has a certain property and then check the value of another property, so ideally the code should be something like this:
graphDatabaseService.registerTransactionEventHandler(new TransactionEventHandler.Adapter<Object>() {
#Override
public Object beforeCommit(TransactionData data) throws Exception {
for (Node node : data.deletedNodes()) {
if (node.hasProperty("checkProperty")){
if (node.hasProperty("propertyToCheck")){
String value = (String) node.getProperty("propertyToCheck");
... do checking on value
}
}
}
return null;
}
But this fails with exception
Caused by: java.lang.IllegalStateException: Node[11] has been deleted in this tx
at org.neo4j.kernel.impl.core.WritableTransactionState$CowEntityElement.assertNotDeleted(WritableTransactionState.java:141)
at org.neo4j.kernel.impl.core.WritableTransactionState$CowEntityElement.getPropertyAddMap(WritableTransactionState.java:129)
at org.neo4j.kernel.impl.core.WritableTransactionState$CowNodeElement.getPropertyAddMap(WritableTransactionState.java:155)
at org.neo4j.kernel.impl.core.WritableTransactionState.getCowPropertyAddMap(WritableTransactionState.java:529)
at org.neo4j.kernel.impl.core.Primitive.hasProperty(Primitive.java:306)
at org.neo4j.kernel.impl.core.NodeImpl.hasProperty(NodeImpl.java:53)
at org.neo4j.kernel.impl.core.NodeProxy.hasProperty(NodeProxy.java:160)
The only workaround I found is this:
for (Node node : data.deletedNodes()) {
boolean check = false;
String valueToCheck = null;
for (PropertyEntry prop : data.removedNodeProperties()) {
if (node.equals(prop.entity())) {
if (prop.key().equals("checkProperty")) {
check = true;
}
if (prop.key().equals("propertyToCheck")) {
valueToCheck = (String) prop.previouslyCommitedValue();
}
}
}
if (check){
... do checking on value
}
}
But this goes through ALL deleted properties, so this is obviously not a nice solution.
So my question is: is there a better way to do this?
Using neo4j 1.9.3
Since the code in TransactionEventHandler#beforeCommit is itself part of the transaction you cannot access any property on a deleted node or relationship. As you've discovered the only way to access deleted properties is via TransactionData#removedNodeProperties() and TransactionData#removedRelationshipProperties().
You can optimize your code by running a single iteration over removedNodeProperties() (just pseudo code below):
for (PropertyEntry<Node> pe: data.removedNodeProperties()) {
if (pe.key().equals("checkProperty")) {
runCheckForDeletedNodeAndValue(pe.entity(), pe.previouslyCommitedValue())
}
}
public void runCheckForDeletedNodeAndValue(Node node, Object oldValue) {
// throw exception if current user is not allowed to delete
// this will rollback whole transaction
}
Your snippet would iterate this collection for each deleted node.
Related
I have a StateFlow from which my List composable collects any changes as a State.
private val _people = MutableStateFlow(personDataList())
val people = _people.asStateFlow()
And inside my viewModel, I perform modifications on _people and I verify that people as a read-only StateFlow is also getting updated. I also have to make a copy of the original _people as an ordinary kotlin map to use for some verifications use-cases.
val copyAsMap : StateFlow<MutableMap<Int, Person>> = people.map {
it.associateBy( { it.id }, { it } )
.toMutableMap()
}.stateIn(viewModelScope, SharingStarted.Eagerly, mutableMapOf())
however, with my attempt above, it (the copyAsMap) doesn't get updated when I try to modify the list (e.g delete) an item from the _people StateFlow
Any ideas..? Thanks!
Edit:
Nothing is collecting from the copyAsMap, I just display the values everytime an object is removed from _person state flow
delete function (triggered by an action somewhere)
private fun delete(personModel: Person) {
_person.update { list ->
list.toMutableStateList().apply {
removeIf { it.id == personModel.id }
}
}
copyAsMap.values.forEach {
Log.e("MapCopy", "$it")
}
}
So based on your comment how you delete the item, that's the problem:
_people.update { list ->
list.removeIf { it.id == person.id }
list
}
You get an instance of MutableList here, do the modification and you "update" the flow with the same instance. And, as StateFlow documentation says:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
Which means that your updated list is actually never emitted, because it is equal to the previous value.
You have to do something like this:
_people.update { list ->
list.toMutableList().apply { removeIf { ... } }
}
Also, you should define your state as val _people: MutableStateFlow<List<T>> = .... This would prevent some mistakes you can make.
At my Neo4j/SDN 4 project I have a following entity:
#NodeEntity
public class Value extends BaseEntity {
#Index(unique = false)
private Object value;
private String description;
...
}
During the application run-time I want to be able to add a new dynamic properties to Value node, like for example value_en_US, value_fr_FR.
Right now I don't know what exact properties will be added to a particular Value node during application run-time so I can't define these properties at the code as a separate fields in Value.
Is there at SDN 4 any mechanisms to define these properties during the application run-time? I need something similar to DynamicProperties from SDN 3.
There is no such functionality in SDN 4, but it will be added in SDN 5 through a #Properties annotation on Map.
It will be available for testing in snapshot version very soon.
Check out this commit for more details
You might also want to look at this response to a similar question.
https://stackoverflow.com/a/42632709/5249743
Just beware that in that answer the function:
public void addAllFields(Class<?> type) {
for (Field field : type.getDeclaredFields()) {
blacklist.add(field.getName());
}
if (type.getSuperclass() != null) {
addAllFields(type.getSuperclass());
}
}
is not bullet proof. For one thing it doesn't look at #Property annotations. So if you want to go down that route keep your eyes open.
An 'improvement' is
public void addAllFields(Class<?> type) {
for (Field field : type.getDeclaredFields()) {
blacklist.add(findName(field));
}
if (type.getSuperclass() != null) {
addAllFields(type.getSuperclass());
}
}
private String findName(Field field) {
Property property = field.getAnnotation(Property.class);
if(property == null || "".equals(property.name())) {
return field.getName();
} else {
return property.name();
}
}
But this obviously doesn't look for the annotation on methods...
The code is on DartPad if you need a complete example (see the while loop towards the end.)
I have a loop,
Place place = places[0];
while (places.isNotEmpty) {
// Get a list of places within distance (we can travel to)
List reachables = place.getReachables();
// Get the closest reachable place
Place closest = place.getClosest(reachables);
// Remove the current place (ultimately should terminate the loop)
places.remove(place);
// Iterate
place = closest;
}
But it's not removing place on the second-to-last line. i.e., the length of the places list remains the same, making it an infinite loop. What's wrong?
This could be because the object in the list has a different hashCode from the object you are trying to remove.
Try using this code instead, to find the correct object by comparing the objects properties, before removing it:
var item = list.firstWhere((x) => x.property1== myObj.property1 && x.property2== myObj.property2, orElse: () => null);
list.remove(item);
Another option is to override the == operator and hashCode in your class.
class Class1 {
#override
bool operator==(other) {
if(other is! Class1) {
return false;
}
return property1 == (other as Class1).property1;
}
int _hashCode;
#override
int get hashCode {
if(_hashCode == null) {
_hashCode = property1.hashCode
}
return _hashCode;
}
}
I have faced the very same issue. Unfortunately I haven't found the root cause, but in the same situation I replaced
places.remove[place]
with
places.removeWhere(p => p.hachCode == place.hashCode)
as a workaround. One more approach was helpful too:
// Get the place from your set:
final place = places.first;
// Replace the place in the set:
places.add(place);
// Remove the place from the set:
places.remove(place);
Most likely place is not in the list for some reason. It's hard to debug without knowing the exact data used, the problem doesn't reproduce with the three-place sample you have in the linked DartPad.
Try figuring out which element is causing the problem. For example you can
try adding an if (!places.contains(place)) print("!!! $place not in $places"); before the remove, or something similar that detects the state when the problem occurs.
This way you can remove object from dynamic list
List data = [
{
"name":"stack"
},
{
"name":"overflow"
}
];
data.removeWhere((item) => item["name"]=="stack");
print(data);
Output
[{name: overflow}]
Use the plugin Equatable
class Place extends Equatable {
...
}
https://pub.dev/packages/equatable
I was having the same issue. I did something like this using removeWhere.
myList.removeWhere((item) => item.id == yourItemId.id)
Is there a built-in / easy way to set mappings between domain class properties and JSON strings that don't have exact matches for the property names?
For example, when I have a domain class:
class Person {
String jobTitle
String favoriteColor
static constraints = {
jobTitle(blank: false)
favoriteColor(blank: false)
}
}
And someone's giving me the following JSON:
{ "currentJob" : "secret agent", "the-color" : "red" }
I'd like to be able to still do this:
new Person(request.JSON).save()
Is there a way in groovy/grails for me to map currentJob -> jobTitle and the-color -> favorite color?
EDIT:
I've done a little experimenting, but I still haven't gotten it working. But I have found out a couple interesting things...
At first I tried overwriting the setProperty method:
#Override
setProperty(String name, Object value) {
if(this.hasProperty(name)) this[name] = value
else {
switch(name) {
'currentJob': this.jobTitle = value; break;
'the-color': this.favoriteColor = value; break;
}
}
}
But this doesn't work for two reasons: 1) setProperty is only called if there is a property that matches name and 2) "this[name] = value" calls setProperty, leading to an infinite recursive loop.
So then I thought, well screw it, I know what the incoming json string looks like (If only I could control it), I'll just get rid of the line that handles the scenario where the names match and I'll override hasProperty, maybe that will work:
#Override
void setProperty(String name, Object value) {
switch(name) {
'currentJob': this.jobTitle = value; break;
'the-color': this.favoriteColor = value; break;
}
}
#Override
boolean hasProperty(String name) {
if(name == "currentJob" || name == "the-color") return true
return false
}
But no, that didn't work either. By a random stroke of luck I discovered, that not only did I have to overwrite hasProperty(), but I also had to have an empty setter for the property.
void setCurrentJob(){ }
That hack worked for currentJob - I guess setProperty only gets called if hasProperty returns true and there is a setter for the property (Even if that setter is auto generated under the covers in grails). Unfortunately I can't make a function "setThe-Color" because of the dash, so this solution doesn't work for me.
Still stuck on this, any help would definitely be appreciated.
EDIT:
Overriding the void propertyMissing(String name, Object value){} method is called by this:
Person person = new Person()
person["currentJob"] = "programmer"
person["the-color"] = "red"
But not by this:
Person person = new Person(["currentJob":"programmer", "the-color":"red"])
In my current project I'm unable to persists a one-to-many relationship.
Both sides of the relationship will be saved - just the link between them is missing.
The one-to-many relationship between my two domains looks like this :
class Parent {
String name
...
static hasMany = [childs: Child]
Parent() {}
Parent(SomeOtherClass obj) {
this.properties = obj.properties
}
}
class Child {
int code
String description
}
I'm creating my Parent instances within a ParentService:
public Boolean createParents(List parentIds) {
boolean savedSuccessfully = true
Parent.withTransaction {status ->
parentIds.each { parentIdString ->
if (parentIdString && parentIdString.isInteger()) {
int parentId = parentIdString.toInteger()
def parentInstance = new Parent(someOtherService.getExternalParentObj(parentId))
someOtherService.getExternalChilds(parentId).each { entry ->
parentInstance.addToChilds(Child.findOrSaveWhere(code: entry.key, description: entry.value))
}
if(!parentInstance.save()) {
status.setRollbackOnly()
savedSuccessfully = false
}
}
}
}
return savedSuccessfully
}
Both, the Parent instances and the Child instances are created and saved successfully. Just the link between parent and child is missing. The childs property of each Parent instance is an empty list.
I dont't know what is wrong here.
Why does the relationship won't be persited? Any ideas?
Update
I've added an integration test for the ParentService to test the existence of the childs list after creating all Parent instances :
...
assert parentService.createParents(["123","234","356"]) == true
def parent = Parent.get(1)
assert parent.childs.size() > 0
...
Unexpectedly - the test passed.
I found the origin of the error.
I totally missed that some entry.value are null. Don't know why I didn't see it during debugging.
Now I only use Child.findOrSaveWhere() if entry.value is not null.
I would have expected that the whole parentInstance.save() would pass or fail completely. Still don't understand why the join table won't be filled without giving any error.