Dynamic Datasource change to show only a subset of the data (survey questions-answers)

Hi,

i created an example for a problem i currently have. It’s a little bit about dynamic datasources and about related reference data - i’ll try to give you an explanation from the example application (which can be found here). Although i know that there are several questions around datasources and how to dynamically modify them, i wasn’t really able to get this case covered (or overlooked it):

It is a survey application where people can create surveys and other people can fill in an existing survey. The entity model looks like this:

The entities for the reference data are:

  • Survey
  • Question
  • PossibleAnswer

The entities for the survey data are:

  • FilledSurvey
  • SelectedAnswer

Attached you’ll find an UML class diagram that shows the relationships (survey-entity-model.png).

What i want to achieve is the following: In the FilledSurvey Editor (filledSurvey-option-datasources.png) i want to list all questions that are related to the selected survey. For each Question i want to create an instance of a SelectedAnswer and list these instances in the table.
The first column of the table is the question. In the second column “answer” the user should be able to select one of the possibleAnswers for this question and the selected should be stored in the attribute answer of the SelectedAnswer entity.

I played with different options here for a while.

The first option was just to render a standard column for the answer attribute. This works but i was not able to restrict the select options only to this one that are related to the question in this row.

In the second implementation column “filteredAnswer” i created the column dynamically and used a nested-nested-nested datasource of the editor in order to display only the possible answers of the question of the current table row (see filled-survey-edit.xml as well):

<datasource id="filledSurveyDs"
                    class="com.company.survey.entity.survey.FilledSurvey"
                    view="filledSurvey-view">
            <collectionDatasource id="answersDs"
                                  property="answers">
                <datasource id="questionsDs" property="question">
                    <collectionDatasource id="questionsPossibleAnswersDs" property="possibleAnswers" />
                </datasource>
            </collectionDatasource>
        </datasource>

The Editor with the generated column looks like this:

public class FilledSurveyEdit extends AbstractEditor<FilledSurvey> {
// ...
    @Inject
    CollectionDatasource<PossibleAnswer, UUID> questionsPossibleAnswersDs

    @Override
    protected void initNewItem(FilledSurvey item) {

        item.filledDate = new Date()
        item.user = userSession.user
        item.answers = []

        survey.addValueChangeListener(new Component.ValueChangeListener() {
            @Override
            public void valueChanged(Component.ValueChangeEvent e) {

                FilledSurvey filledSurvey = getItem()
                Survey survey = (Survey) e.getValue();

                survey.questions.each {
                    SelectedAnswer answer = metadata.create(SelectedAnswer.class)
                    answer.question = it
                    answer.filledSurvey = filledSurvey

                    filledSurvey.answers << answer
                    answersDs.addItem(answer)
                }
            }
        });
        super.initNewItem(item)
    }

    @Override
    public void init(Map<String, Object> params) {
        super.init(params);
        answersTable.addGeneratedColumn("filteredAnswer", new Table.ColumnGenerator<SelectedAnswer>() {
            @Override
            Component generateCell(SelectedAnswer entity) {
                LookupPickerField field = componentsFactory.createComponent(LookupPickerField.NAME);
                
               //  All PossibleAnswers should be displayed that belong to this Question (entity.question)
                field.setDatasource(answersTable.getItemDatasource(entity), "answer");
                field.setOptionsDatasource(questionsPossibleAnswersDs);
                return field;
            }
        })
    }
}

This approach works for the new case, where i create a new filled survey. The associations are correctly used of the nested datasource so that only the possible answers of current question in the table are selectable. This is quite amazing since i just tried to nest the datasource as much as i needed to get to the information. Although i don’t really understand what that exactly means and how this even works internally - it seems to work. But unfortunately only for the new case. When i edit a filled survey and change one of the answers i get the following Exception: org.eclipse.persistence.exceptions.ValidationException

2016-09-01 07:59:17.431 ERROR [http-nio-8080-exec-5/app/admin] com.haulmont.cuba.web.log.AppLog - Exception in com.haulmont.cuba.web.toolkit.ui.CubaButton: 
com.vaadin.server.ServerRpcManager$RpcInvocationException: Unable to invoke method click in com.vaadin.shared.ui.button.ButtonServerRpc
 at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:160)
 at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:118)
 at com.vaadin.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:414)
 at com.vaadin.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:274)
 at com.vaadin.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:79)
 at com.haulmont.cuba.web.sys.CubaVaadinServletService$CubaUidlRequestHandler.lambda$synchronizedHandleRequest$92(CubaVaadinServletService.java:307)
 at com.haulmont.cuba.web.sys.CubaVaadinServletService.withUserSession(CubaVaadinServletService.java:189)
 at com.haulmont.cuba.web.sys.CubaVaadinServletService$CubaUidlRequestHandler.synchronizedHandleRequest(CubaVaadinServletService.java:307)
 at com.vaadin.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:41)
 at com.vaadin.server.VaadinService.handleRequest(VaadinService.java:1409)
 at com.vaadin.server.VaadinServlet.service(VaadinServlet.java:369)
 at com.haulmont.cuba.web.sys.CubaApplicationServlet.serviceAppRequest(CubaApplicationServlet.java:254)
 at com.haulmont.cuba.web.sys.CubaApplicationServlet.service(CubaApplicationServlet.java:163)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:292)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
 at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
 at com.haulmont.cuba.web.sys.CubaHttpFilter.handleNotFiltered(CubaHttpFilter.java:108)
 at com.haulmont.cuba.web.sys.CubaHttpFilter.doFilter(CubaHttpFilter.java:95)
 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
 at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
 at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
 at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
 at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
 at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616)
 at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
 at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:528)
 at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1099)
 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1520)
 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1476)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
 at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:497)
 at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:158)
 ... 38 more
Caused by: com.vaadin.event.ListenerMethod$MethodException: Invocation of method buttonClick in com.haulmont.cuba.web.gui.components.WebButton$1 failed.
 at com.vaadin.event.ListenerMethod.receiveEvent(ListenerMethod.java:528)
 at com.vaadin.event.EventRouter.fireEvent(EventRouter.java:198)
 at com.vaadin.event.EventRouter.fireEvent(EventRouter.java:161)
 at com.vaadin.server.AbstractClientConnector.fireEvent(AbstractClientConnector.java:1030)
 at com.vaadin.ui.Button.fireClick(Button.java:377)
 at com.haulmont.cuba.web.toolkit.ui.CubaButton.fireClick(CubaButton.java:54)
 at com.vaadin.ui.Button$1.click(Button.java:54)
 ... 43 more
Caused by: com.vaadin.ui.Table$CacheUpdateException: Error during Table cache update.
 at com.vaadin.ui.Table.maybeThrowCacheUpdateExceptions(Table.java:1800)
 at com.vaadin.ui.Table.refreshRenderedCells(Table.java:1789)
 at com.vaadin.ui.Table.refreshRowCache(Table.java:2729)
 at com.vaadin.ui.Table.containerItemSetChange(Table.java:4666)
 at com.haulmont.cuba.web.gui.data.CollectionDsWrapper.fireItemSetChanged(CollectionDsWrapper.java:112)
 at com.haulmont.cuba.web.gui.data.CollectionDsWrapper$ContainerDatasourceCollectionChangeListener.collectionChanged(CollectionDsWrapper.java:263)
 at com.haulmont.cuba.gui.data.impl.WeakCollectionChangeListener.collectionChanged(WeakCollectionChangeListener.java:41)
 at com.haulmont.cuba.gui.data.impl.CollectionPropertyDatasourceImpl.fireCollectionChanged(CollectionPropertyDatasourceImpl.java:731)
 at com.haulmont.cuba.gui.data.impl.CollectionPropertyDatasourceImpl.committed(CollectionPropertyDatasourceImpl.java:703)
 at com.haulmont.cuba.gui.data.impl.DsContextImpl.notifyAllDsCommited(DsContextImpl.java:222)
 at com.haulmont.cuba.gui.data.impl.DsContextImpl.commit(DsContextImpl.java:168)
 at com.haulmont.cuba.gui.components.EditorWindowDelegate.commit(EditorWindowDelegate.java:255)
 at com.haulmont.cuba.web.gui.WebWindow$Editor.commitAndClose(WebWindow.java:1437)
 at com.haulmont.cuba.gui.components.AbstractEditor.commitAndClose(AbstractEditor.java:110)
 at com.haulmont.cuba.gui.components.EditorWindowDelegate$2.actionPerform(EditorWindowDelegate.java:93)
 at com.haulmont.cuba.web.gui.components.WebButton.performAction(WebButton.java:48)
 at com.haulmont.cuba.web.gui.components.WebButton$1.buttonClick(WebButton.java:39)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:497)
 at com.vaadin.event.ListenerMethod.receiveEvent(ListenerMethod.java:508)
 ... 49 more
Caused by: Exception [EclipseLink-7242] (Eclipse Persistence Services - 2.6.2.cuba6): org.eclipse.persistence.exceptions.ValidationException
Exception Description: An attempt was made to traverse a relationship using indirection that had a null Session.  This often occurs when an entity with an uninstantiated LAZY relationship is serialized and that relationship is traversed after serialization.  To avoid this issue, instantiate the LAZY relationship prior to serialization.
 at org.eclipse.persistence.exceptions.ValidationException.instantiatingValueholderWithNullSession(ValidationException.java:1024)
 at org.eclipse.persistence.internal.indirection.UnitOfWorkValueHolder.instantiate(UnitOfWorkValueHolder.java:233)
 at org.eclipse.persistence.internal.indirection.DatabaseValueHolder.getValue(DatabaseValueHolder.java:101)
 at org.eclipse.persistence.indirection.IndirectSet.buildDelegate(IndirectSet.java:218)
 at org.eclipse.persistence.indirection.IndirectSet.getDelegate(IndirectSet.java:388)
 at org.eclipse.persistence.indirection.IndirectSet$1.(IndirectSet.java:461)
 at org.eclipse.persistence.indirection.IndirectSet.iterator(IndirectSet.java:460)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:497)
 at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
 at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
 at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1210)
 at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1019)
 at org.codehaus.groovy.runtime.InvokerHelper.invokePojoMethod(InvokerHelper.java:905)
 at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:896)
 at org.codehaus.groovy.runtime.InvokerHelper.asIterator(InvokerHelper.java:571)
 at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.getPropertySpreadSafe(ScriptBytecodeAdapter.java:471)
 at com.company.survey.web.survey.filledsurvey.FilledSurveyEdit$2.generateCell(FilledSurveyEdit.groovy:95)
 at com.company.survey.web.survey.filledsurvey.FilledSurveyEdit$2.generateCell(FilledSurveyEdit.groovy)
 at com.haulmont.cuba.web.gui.components.WebAbstractTable$4.generateCell(WebAbstractTable.java:1439)
 at com.vaadin.ui.Table.parseItemIdToCells(Table.java:2402)
 at com.vaadin.ui.Table.getVisibleCellsNoCache(Table.java:2275)
 at com.vaadin.ui.Table.refreshRenderedCells(Table.java:1778)
 ... 69 more

Here you’ll find the views.xml (in case something is wrong with this).

I don’t really get what the exception wants to tell me. Additionally in the UI the following error occurs: exception-with-missing-picker-field.png - which indicates the error on the UI but also shows that in the background the picker field that i used to change the value could not be painted on the screen anymore. The error occurs when i select “Save” in the editor.

So here’s my question (after this fairly long explanation):
Do you either know what i have to change in the first approach to just show me the possible answers for this question, or do you know what i have to change in the second approach in order to make the error disappear?
Perhaps you might be able to give a rough explenation how this nested-nested-nested datasource beast works under the hood - because right now this is a fair amount of magic going on…

Bye,
Mario

survey-entity-model

filledSurvey-option-datasources

exception-with-missing-picker-field

Hi Mario,
The first approach is obviously not suitable because you need different options for each row, hence different option datasources.

In the second approach, instead of defining deeply nested datasources and views, I would suggest creating datasources for Filtered Answers column programmatically via DsBuilder:

CollectionDatasource optionsDs = new DsBuilder(getDsContext())
        .setJavaClass(PossibleAnswer.class)
        .setViewName(View.MINIMAL)
        .buildCollectionDatasource()
optionsDs.setQuery('select e from survey$PossibleAnswer e where e.question.id = :custom$question')
optionsDs.refresh(['question': entity.question])

field.setOptionsDatasource(optionsDs);

//showNotification("${entity.question.possibleAnswers*.answerText.join(", ")}", Frame.NotificationType.TRAY)

This is not an optimal solution, because there may be excessive queries, but it can be improved by caching these datasources in the screen.

I also had to comment out your showNotification output as it causes the unfetched attribute error on saving edited survey.

Hi Konstantin,

thanks for your answer. That did the trick. I wasn’t really aware of the DsBuilder class - thanks for that. Is this the “best” option to go for in this kind of scenarios?

But here’s an additional one:
I added another example with an OptionsGroup instead of an PickerField. The options get displayed, but the selections are not stored. I commited the failing example here:
https://github.com/mariodavid/cuba-example-survey-ui/commit/de7d9121ceab6b69f601198595a78d368aa29c9a
(created another button in the browser go get to this failing example).
The only differences are the following:

OptionsGroup field = componentsFactory.createComponent(OptionsGroup.NAME);
field.orientation = OptionsGroup.Orientation.HORIZONTAL

Do you know where the difference between these two implementations is?

Bye,
Mario

I think DsBuilder should be used when the declarative creation of datasources is impossible or inconvenient - as in your case.
Regarding OptionsGroup - it requires some investigation, we will let you know what’s wrong with it later.

Hi Konstantin,

did you had the time to look at the option group problem? i can’t really see where the difference to the working example with a PickerField.

Any help is appreciated.

Bye,
Mario

Hi Mario,

It’s a bug in OptionsGroup, and it is still open. We’ll try to fix it shortly.

Hi,

There is a workaround in version 6.2. You can change the order of calls: setOptionsDatasource and setDatasource to set data source after options data source is assigned.


field.setOptionsDatasource(optionsDs);
field.setDatasource(answersTable.getItemDatasource(entity), "answer")

We will release fix for this issue in version 6.2.8.

hi,

thanks for the workaround & for the bugfix

Bye,
Mario

Hi Konstantin
I was also trying to use options groups to display differently through Datasource and seems like it is not working due to this bug. May we expect this will be fixed in V 6.3?

Sure, it will be fixed in 6.2.8 and 6.3.0.

Thank you

Fixed in 6.2.8.