primefaces icon indicating copy to clipboard operation
primefaces copied to clipboard

Timeline: null value when used inside a composite

Open stefanrosca opened this issue 4 years ago • 18 comments

Describe the defect Timeline shows no events

Environment:

  • PF Version: 8.0, 10RC1
  • JSF + version: Mojarra 2.3
  • Affected browsers: ALL_

To Reproduce Steps to reproduce the behavior:

  1. create a composite component and put a timeline in it
  2. enable lazy loading
  3. use composite component attribute for timeline "value" (eg. value="#{cc.attrs.model}")
  4. See error SEVERE: Exception invoking UIViewRoot PhaseListener org.primefaces.component.timeline.DefaultTimelineUpdater. and empty timeline

Expected behavior The timeline should load and display events

compositeTimeline.xhtml

<ui:component xmlns="http://www.w3.org/1999/xhtml"
              xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
              xmlns:cc="http://xmlns.jcp.org/jsf/composite"
              xmlns:p="http://primefaces.org/ui">
    <cc:interface>
        <cc:attribute name="value" type="org.primefaces.model.timeline.TimelineModel" required="true" />
    </cc:interface>

    <cc:implementation>
        <p:timeline id="timeline" value="#{cc.attrs.value}"
                    preloadFactor="#{lazyTimelineView.preloadFactor}"
                    zoomMax="#{lazyTimelineView.zoomMax}" minHeight="170">
            <p:ajax event="lazyload" update="@none" listener="#{lazyTimelineView.onLazyLoad}"
                    onstart="$('#loadingText').css('visibility', 'visible')"
                    oncomplete="$('#loadingText').css('visibility', 'hidden')"/>
        </p:timeline>
    </cc:implementation>
</ui:component>

test.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:p="http://primefaces.org/ui"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:cc="http://java.sun.com/jsf/composite/cc">

    <h:head>
        <title>PrimeFaces Test</title>
        <h:outputScript name="test.js" />
    </h:head>
    <h:body>
        <div class="card">
            <h:form id="form">
                <p:growl id="growl" showSummary="true" showDetail="false">
                    <p:autoUpdate/>
                </p:growl>

                <div id="loadingText" style="font-weight:bold; margin:-5px 0 5px 0; visibility:hidden;">Loading ...
                </div>

                <cc:compositeTimeline id="compositeTimeline" value="#{lazyTimelineView.model}" />

                <h:panelGrid columns="2" style="margin-top:15px">
                    <p:spinner id="spinner" value="#{lazyTimelineView.preloadFactor}"
                               min="0" max="1" stepFactor="0.05"/>
                    <p:commandButton value="Update Preload Factor" process="@this spinner" update="compositeTimeline"
                                     action="#{lazyTimelineView.clearTimeline}"
                                     style="margin-left:5px"/>
                </h:panelGrid>
            </h:form>
        </div>
    </h:body>
</html>

LazyTimelineView

@Named
@ViewScoped
public class LazyTimelineView implements Serializable {

    private TimelineModel<String, ?> model;

    private float preloadFactor = 0;
    private long zoomMax;

    @PostConstruct
    protected void initialize() {
        // create empty model
        model = new TimelineModel<>();

        // about five months in milliseconds for zoomMax
        // this can help to avoid a long loading of events when zooming out to wide time ranges
        zoomMax = 1000L * 60 * 60 * 24 * 31 * 5;
    }

    public TimelineModel<String, ?> getModel() {
        return model;
    }

    public void onLazyLoad(TimelineLazyLoadEvent e) {
        try {
            // simulate time-consuming loading before adding new events
            Thread.sleep((long) (1000 * Math.random() + 100));
        } catch (InterruptedException ex) {
            // ignore
        }

        TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(((Timeline)e.getSource()).getClientId());

        LocalDateTime startDate = e.getStartDateFirst(); // alias getStartDate() can be used too
        LocalDateTime endDate = e.getEndDateFirst(); // alias getEndDate() can be used too

        // fetch events for the first time range
        generateRandomEvents(startDate, endDate, timelineUpdater);

        if (e.hasTwoRanges()) {
            // zooming out ==> fetch events for the second time range
            generateRandomEvents(e.getStartDateSecond(), e.getEndDateSecond(), timelineUpdater);
        }
    }

    private void generateRandomEvents(LocalDateTime startDate, LocalDateTime endDate, TimelineUpdater timelineUpdater) {
        LocalDateTime curDate = startDate;
        Random rnd = new Random();
        PrimitiveIterator.OfInt randomInts = rnd.ints(1, 99999).iterator();

        while (curDate.isBefore(endDate)) {
            // create events in the given time range
            if (rnd.nextBoolean()) {
                // event with only one date
                model.add(TimelineEvent.<String>builder()
                        .data("Event " + randomInts.nextInt())
                        .startDate(curDate)
                        .build(), timelineUpdater);
            } else {
                // event with start and end dates
                model.add(TimelineEvent.<String>builder()
                        .data("Event " + randomInts.nextInt())
                        .startDate(curDate)
                        .endDate(curDate.plusHours(18))
                        .build(),
                        timelineUpdater);
            }

            curDate = curDate.plusHours(24);
        }
    }

    public void clearTimeline() {
        // clear Timeline, so that it can be loaded again with a new preload factor
        model.clear();
    }

    public void setPreloadFactor(float preloadFactor) {
        this.preloadFactor = preloadFactor;
    }

    public float getPreloadFactor() {
        return preloadFactor;
    }

    public long getZoomMax() {
        return zoomMax;
    }
}

stefanrosca avatar Feb 04 '21 14:02 stefanrosca

Can you zip your reproducer and attach it here please?

melloware avatar Feb 04 '21 15:02 melloware

Thanks makes our lives much easier.

melloware avatar Feb 04 '21 15:02 melloware

When running with -Pmyfaces23 I get this stack trace..

Feb 04, 2021 1:05:11 PM javax.faces.component.UIViewRoot notifyListeners
SEVERE: An Exception occured while processing the beforePhase method of PhaseListener org.primefaces.component.timeline.DefaultTimelineUpdater@26490518 in Phase RENDER_RESPONSE(6)
java.lang.NullPointerException
        at org.primefaces.component.timeline.TimelineRenderer.calculateGroupsFromModel(TimelineRenderer.java:543)
        at org.primefaces.component.timeline.DefaultTimelineUpdater.processCrudOperations(DefaultTimelineUpdater.java:144)
        at org.primefaces.component.timeline.DefaultTimelineUpdater.beforePhase(DefaultTimelineUpdater.java:116)
        at javax.faces.component.UIViewRoot.notifyListeners(UIViewRoot.java:1099)
        at javax.faces.component.UIViewRoot.encodeBegin(UIViewRoot.java:531)
        at javax.faces.component.UIComponentBase.encodeAll(UIComponentBase.java:527)
        at org.apache.myfaces.view.facelets.FaceletViewDeclarationLanguage.renderView(FaceletViewDeclarationLanguage.java:1897)
        at org.apache.myfaces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:315)

melloware avatar Feb 04 '21 18:02 melloware

I think both your issues are related right? Do either of your tickets work WITHOUT lazy loading?

melloware avatar Feb 05 '21 22:02 melloware

Arf doesn't look good... specially if same problem happen with both impl... I tried without a composite and works fine.

If you debug Timeline#getValue, you'll see that it's trying to put the composite in ELContext so the value gets properly parsed (see ContextualCompositeValueExpression) but it doesn't seem to "find" the compositeTimeLine.xhtml (see CompositeComponentStackManager#findCompositeComponentUsingLocation). No composite found, no value, then NPE

Rapster avatar Feb 06 '21 12:02 Rapster

I wans't sure if this had to do with the DefaultTimeLineUpdater PhaseListener either as the other issue gets an NPE there trying to get the model in Lazy Load.

melloware avatar Feb 06 '21 13:02 melloware

in general i dont like how the DefaultTimeLineUpdater is implemented, maybe we can think about better ideas in 11.0

tandraschko avatar Feb 06 '21 20:02 tandraschko

I started few weeks ago on refactoring timeline, nothing big. I'll submit a PR for review and hopefully ready for 11

Rapster avatar Feb 06 '21 20:02 Rapster

This has just punched me in the face... Is there any workaround? Do you have any idea?

liptga avatar Nov 02 '22 08:11 liptga

Arf doesn't look good... specially if same problem happen with both impl... I tried without a composite and works fine.

Yes, don't use composite 😄 But on a more serious note, I didn't dig that much since last time, so that's my best answer so far

Rapster avatar Nov 02 '22 08:11 Rapster

I don't give up that easy. I have a workaround if the composite component is used in a view only once:

Composite component class:

	@FacesComponent("mycc")
	public class MyCC {
		private static final String REQUEST_MAP_KEY = "myccRequestVariable";

		/**
		 * Workaround for https://github.com/primefaces/primefaces/issues/6944
		 * Idea comes from https://stackoverflow.com/a/11961132
		 */
		public MyCC() {
			Map<String, Object> requestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestMap();
			if ( requestMap.containsKey(REQUEST_MAP_KEY) ) {
				//Throwing exception if the component is more than once in view
				throw new RuntimeException("Only one " + MyCC.class.getName() + " work in a page. " +
						"Cause is a workaround for a bug https://github.com/primefaces/primefaces/issues/6944!");
			} else {
				requestMap.put(REQUEST_MAP_KEY, this);
			}
		}

		public TimelineModel<X, Y> getTimelineModel() {
			....;
		}

		public void onLazyLoad(TimelineLazyLoadEvent e) {
			....;
		}
	}

CC xhtml:

		<p:timeline value="#{myccRequestVariable.timelineModel}"
					start="#{myccRequestVariable.attrs.startDate}"
					end="#{myccRequestVariable.attrs.endDate}"
					widgetVar="timelineWdgt">
					<p:ajax event="lazyload" update="@none" listener="#{myccRequestVariable.onLazyLoad}"/>
		</p:timeline>

Better than nothing.

liptga avatar Nov 02 '22 10:11 liptga

Yeah... First, like Thomas mentionned the component needs some love (I have to check if I can find the refacto I started few years back...) and second, you better off using facelet tags (in combination with omnifaces) than composite (works as an include, do better and simpler than cc) and you won't have to patch anything

Rapster avatar Nov 02 '22 10:11 Rapster

Yeah... First, like Thomas mentionned the component needs some love (I have to check if I can find the refacto I started few years back...) and second, you better off using facelet tags (in combination with omnifaces) than composite (works as an include, do better and simpler than cc) and you won't have to patch anything

The problem is, that if you want to have state, it is better to use composite, as far as I understand CC. But of course if you can have only one instance in the view, it can be a view scope bean, who keeps the state, I know.

liptga avatar Nov 02 '22 11:11 liptga

Not sure I follow, every UIComponent has a state...If by state, you mean your bean then I'm not aware of the relation between this and the type of reusable component you wanna use...

At this point, either you wanna to keep using a cc then you have to stick to your patch (till we refactor this component, and yet there is no guarantee we get rid of PhaseListener, I must say I have no idea why this is used...), or go for an alternative such as facelet tag (which is better IMO) and most probably everything will work fine (i'm a cc hater, so I'm biased 😅)

Rapster avatar Nov 02 '22 11:11 Rapster

Not sure I follow, every UIComponent has a state...If by state, you mean your bean then I'm not aware of the relation between this and the type of reusable component you wanna use...

At this point, either you wanna to keep using a cc then you have to stick to your patch (till we refactor this component, and yet there is no guarantee we get rid of PhaseListener, I must say I have no idea why this is used...), or go for an alternative such as facelet tag (which is better IMO) and most probably everything will work fine (i'm a cc hater, so I'm biased 😅)

I also don't like CCs, since somehow it never works as expected. Still I have not seen possibilities to have a state associated with a facelet tag "instance". Thats the problem.

liptga avatar Nov 02 '22 13:11 liptga

Again, I have no idea what you mean by state, so I think a bit of reading is required: https://stackoverflow.com/a/6822269/4605161

Rapster avatar Nov 02 '22 13:11 Rapster

The link you found is really useful to understand the issue, plus it's BalusC certified therefore I'm posting just to highlight it 😄 : https://stackoverflow.com/a/11961132/4605161

Rapster avatar Nov 02 '22 13:11 Rapster