Attempt to set Session Attribute via Group Listener throws unique id exception

Hi,

I’m Following the guidelines for a multi tenancy case. (see (https://www.cuba-platform.com/discuss/t/implement-multitenancy-through-access-groups#comment-7029)

I set a BeforeInsertEntityListener to set a SessionAttribute automatically, after some tries I managed to have the new group with the Session Attribute stored in the DB, but when I do, I get a unique id violation since I’m persisting “entity” before insert

Here is the code

@Component("girasole_NewGroupEntityListener")
public class NewGroupEntityListener implements BeforeInsertEntityListener<Group> {

    @Inject
    private Metadata metadata;

    @Inject
    private Persistence persistence;

    @Override
    public void onBeforeInsert(Group entity, EntityManager entityManager) {

        Group parent = entity.getParent();

        if (parent.getName().equals("Girasole")) {
            TypedQuery<Group> query = persistence.getEntityManager().createQuery(
                    "select g from sec$Group g", Group.class
            );
            List<Group> groups = query.getResultList();
            Integer maxCompanySessionId = 0;

            for (Group group: groups) {
                Set<SessionAttribute> attributes = group.getSessionAttributes();
                for (SessionAttribute attribute: attributes) {
                    if (attribute.getName().equals("companySessionId")) {
                        Integer value = Integer.parseInt(attribute.getStringValue());
                        if (value > maxCompanySessionId){
                            maxCompanySessionId = value;
                        }
                    }
                }
            }

            Set<SessionAttribute> sessionAttributes = new HashSet<SessionAttribute>();
            SessionAttribute companySessionId = metadata.create(SessionAttribute.class);
            companySessionId.setName("companySessionId");
            companySessionId.setStringValue(Integer.toString(maxCompanySessionId + 1));
            companySessionId.setDatatype("int");
            sessionAttributes.add(companySessionId);

            try (Transaction tx = persistence.createTransaction()) {
                entity.setSessionAttributes(sessionAttributes);
                persistence.getEntityManager().persist(entity);
                companySessionId.setGroup(entity);
                persistence.getEntityManager().persist(companySessionId);
                tx.commit();
            }

        }

    }

}

Besides I have to refresh the page to see the new group.

I’ve also tried with an AfterInsertEntityListener but the Session Attribute is not set.

Thanks for helping!!!

Lucio

Here is the error log

error_log.txt (9.5K)

Hi,

It’s a proper exception caused by your code. You cannot commit the entity passed to the listener. Here the example of working code:

@Component(GroupEntityListener.NAME)
public class GroupEntityListener implements BeforeInsertEntityListener<Group> {

    public static final String NAME = "sample_GroupEntityListener";

    @Inject
    private Metadata metadata;
    @Inject
    private UniqueNumbersAPI uniqueNumbers;

    @Override
    public void onBeforeInsert(Group entity, EntityManager entityManager) {
        SessionAttribute sessionAttribute = metadata.create(SessionAttribute.class);
        sessionAttribute.setName("someNumber");
        sessionAttribute.setStringValue(Long.toString(uniqueNumbers.getNextNumber(GroupEntityListener.NAME)));
        sessionAttribute.setDatatype(IntegerDatatype.NAME);
        sessionAttribute.setGroup(entity);

        entityManager.persist(sessionAttribute);
    }
}

So, the main difference with your code is that I don’t commit the Group entity, but commit the SessionAttribute entity.
Pay attention that I set value for the Group attribute of the SessionAttribute entity, instead of adding session attributes to the group entity.

sessionAttribute.setGroup(entity);

After that, I persist the SessionAttribute entity:

entityManager.persist(sessionAttribute);

Also, I’ve noticed that you need the “next” number for every new group. If so, I would suggest you using UniqueNumbersAPI for that purpose. You can read more about Sequence Generation in the documentation.
Regards,
Gleb

1 Like

Ok, I think I got this more or less
Check related topic: https://www.cuba-platform.com/discuss/t/use-of-beforedetachentitylistener-after-commit-update
The Listener has to be AfterInsert and the transaction MUST be sent in a separate thread…

@Component("girasole_NewGroupEntityListener")
public class NewGroupEntityListener implements BeforeInsertEntityListener<Group>, AfterInsertEntityListener<Group> {

    @Inject
    private Metadata metadata;

    @Inject
    private Persistence persistence;

    private Integer maxCompanySessionId = 0;

    private ExecutorService executorService = Executors.newFixedThreadPool(10);

    @Override
    public void onBeforeInsert(Group entity, EntityManager entityManager) {

    }

    @Override
    public void onAfterInsert(Group entity, Connection connection) {

        Group parent = entity.getParent();

        if (parent.getName().equals("Parent_Group") && entity.getSessionAttributes().isEmpty()
                && entity.getName().contains("Template_Group_Name")) {
            TypedQuery<Group> query = persistence.getEntityManager().createQuery(
                    "select g from sec$Group g", Group.class
            );
            List<Group> groups = query.getResultList();

            for (Group group : groups) {
                Set<SessionAttribute> attributes = group.getSessionAttributes();
                for (SessionAttribute attribute : attributes) {
                    if (attribute.getName().equals("companySessionId") &! group.getName().equals(entity.getName())) {
                        Integer value = Integer.parseInt(attribute.getStringValue());
                        if (value > maxCompanySessionId) {
                            maxCompanySessionId = value;
                        }
                    }
                }
            }

            updateSessionAttribute(entity, maxCompanySessionId + 1);

        }


    }

    private void updateSessionAttribute(Group group, Integer value) {

        executorService.submit(new SecurityContextAwareRunnable(() -> {

            // some artificial delay
            sleep();

            // your business logic in a new transaction
            Set<SessionAttribute> entitySessionAttributes = group.getSessionAttributes();

            boolean create = true;

            // this is never executed sessionAttributes is always empty
            for (SessionAttribute sa : entitySessionAttributes) {
                if (sa.getName().equals("companySessionId")) {
                    create = false;
                    sa.setStringValue(Integer.toString(maxCompanySessionId + 1));
                    try (Transaction tx = persistence.createTransaction()) {
                        persistence.getEntityManager().merge(sa);
                        tx.commit();
                    }
                    break;
                }
            }

            if (create) {

                SessionAttribute companySessionId = metadata.create(SessionAttribute.class);
                companySessionId.setName("companySessionId");
                companySessionId.setStringValue(Integer.toString(value));
                companySessionId.setDatatype("int");
                companySessionId.setGroup(group);

                try (Transaction tx = persistence.createTransaction()) {
                    group.getSessionAttributes().add(companySessionId);
                    companySessionId.setGroup(group);
                    persistence.getEntityManager().persist(companySessionId);
                    tx.commit();
                }
            }
        }));
    }

    private void sleep() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Forget the BeforeInsertEntityListener part, there are some minor issues anyway:

  • Everything works fine when you create a new group from scratch, but when you copy a group from another, you get always an empty sessionAttributes Set in the Runnable context . Probably because the Group Entity status at onAfterInsert is the one you get before all the remaining attributes and FKs are set no matter how much I sleep().
  • Since you cannot check/update sessionAttributes this way, two Session Attributes with the same name("companySessionId") are created at the end of the copy transaction
  • Attempts to query for groups with EntityManager in the SecurityContextAwareRunnable hangs the system
  • I managed to solve the problem creating a "Template" Group with the intended sub-groups hierarchy but without the sessionAttribute. With this:
            if (parent.getName().equals("Parent_Group") && entity.getSessionAttributes().isEmpty()
                    && entity.getName().contains("Template_Group_Name")) {

    The companySessionId generation is automated only if the new group has a given parent and is copied from the Template Group.

    Of course checking on the name only, does not prevent accidental Template copies-of-copies with double companySessionId, but this is the only thing I could get now…

  • So why isn't it possible to have the updated Session Attributes Set or put a listener at the end of the copy operation?
  • Also realted, why do I need a separate thread onAfterInsert and not onBeforeInsert?

Did you try my solution or it’s not suitable for your needs?

Hello Gleb Thanks for the reply!

Setting Session Atrribute onBeforeInsert and the UniqueNumbersAPI can save me a lot of work, thanks!
But what about the “copy” group problem? I think I’ll still have doubles in the Session Attribute Set

I checked and it does duplicate the Session Attribute in the “copy” case… I guess there’s no easy solution for that besides using the Template Group

I investigated the problem and found a solution.

We have two problems:

  1. While copying, original session attributes are added to a new group
  2. Due to Group copying logic onBeforeInsert is fired twice (We have created a YouTrack issue, see the link on the right.)

As the result, we have three session attributes with the same name for one instance.

For the first problem, you can create a custom realization of UserManagementServiceBean and change group copying logic to skip session attributes with a certain name.

For the second problem, you can check if there are session attributes with a certain name for the creating group, e.g.

@Override
public void onBeforeInsert(Group entity, EntityManager entityManager) {
	// In case of https://youtrack.cuba-platform.com/issue/PL-9350
	// we need to check if session attribute has been added
	TypedQuery<SessionAttribute> query = persistence.getEntityManager().createQuery(
			"select e from sec$SessionAttribute e where e.group.id = ?1 and e.name = ?2", SessionAttribute.class);
	query.setParameter(1, entity.getId());
	query.setParameter(2, "someNumber");

	List<SessionAttribute> sessionAttributes = query.getResultList();

	if (CollectionUtils.isEmpty(sessionAttributes)) {
		SessionAttribute sessionAttribute = metadata.create(SessionAttribute.class);
		sessionAttribute.setName("someNumber");
		sessionAttribute.setStringValue(Long.toString(uniqueNumbers.getNextNumber(GroupEntityListener.NAME)));
		sessionAttribute.setDatatype(IntegerDatatype.NAME);
		sessionAttribute.setGroup(entity);

		entityManager.persist(sessionAttribute);
	}
}

I’ve created a project at GitHub. Take a loot at com.company.sample.listener.GroupEntityListener and com.company.sample.security.app.CustomUserManagementServiceBean.

Regards,
Gleb

1 Like

Thank you Gleb!

Seems neat! I’ll try that

:ticket: See the following issue in our bug tracker:

https://youtrack.cuba-platform.com/issue/PL-9350