Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import org.apache.myfaces.core.api.shared.lang.Assert;
import org.apache.myfaces.view.ViewDeclarationLanguageStrategy;
import org.apache.myfaces.view.ViewMetadataBase;
import org.apache.myfaces.view.ViewScopeProxyMap;
import org.apache.myfaces.view.facelets.compiler.Compiler;
import org.apache.myfaces.view.facelets.compiler.SAXCompiler;
import org.apache.myfaces.view.facelets.el.CompositeComponentELUtils;
Expand Down Expand Up @@ -1897,6 +1898,23 @@ else if (stateWriter.isStateWrittenWithoutWrapper())
}
}
}

// This view was rendered without writing its state (no UIForm, hence no
// view state token), so it can never be restored. Any @ViewScoped beans
// created while building it are unreachable once this request ends, but -
// unlike views whose state is saved - it is never registered in the session
// SerializedViewCollection, so the normal "evict view -> destroy its view
// scope" path never runs and the beans would linger until the session
// expires. Destroy the view scope now (publishes PreDestroyViewMapEvent).
if (!view.isTransient())
{
Map<String, Object> viewMap = view.getViewMap(false);
if (viewMap instanceof ViewScopeProxyMap
&& ((ViewScopeProxyMap) viewMap).getViewScopeId() != null)
{
viewMap.clear();
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.view.facelets.viewscope;

import jakarta.el.ExpressionFactory;
import jakarta.faces.application.ProjectStage;

import org.apache.myfaces.test.core.AbstractMyFacesCDIRequestTestCase;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* Regression test for the @ViewScoped leak on formless views.
*
* <p>A view without any {@code UIForm} never has its state saved, so it can never be restored and
* is never registered in the session {@code SerializedViewCollection}. Repeatedly requesting such a
* view that references a {@code @ViewScoped} bean must not accumulate view scopes in the session:
* the scope has to be destroyed at the end of each request. Before the fix the beans were never
* destroyed and piled up in the session (a memory leak), so {@code DESTROYED} stayed at 0.</p>
*/
public class ViewScopeFormlessLeakTestCase extends AbstractMyFacesCDIRequestTestCase
{

protected ExpressionFactory createExpressionFactory()
{
return new org.apache.el.ExpressionFactoryImpl();
}

@Override
protected void setUpWebConfigParams() throws Exception
{
super.setUpWebConfigParams();
servletContext.addInitParameter("org.apache.myfaces.annotation.SCAN_PACKAGES",
"org.apache.myfaces.view.facelets.viewscope");
servletContext.addInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME, "Production");
}

@Test
public void testFormlessViewDoesNotLeakViewScopedBeans() throws Exception
{
ViewScopeLeakProbeBean.reset();

int requests = 5;
for (int i = 0; i < requests; i++)
{
startViewRequest("/formlessViewScoped.xhtml");
processLifecycleExecute();
renderResponse();
endRequest();
}

Assertions.assertEquals(requests, ViewScopeLeakProbeBean.CREATED.get(),
"the @ViewScoped bean must be created once per request");
Assertions.assertEquals(requests, ViewScopeLeakProbeBean.DESTROYED.get(),
"the @ViewScoped beans of a formless (never-saved) view must be destroyed each "
+ "request, not leaked into the session");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.view.facelets.viewscope;

import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

/**
* A {@code @ViewScoped} CDI bean that counts its creations and destructions, used to assert that
* the view scope of a formless (never-saved) view is destroyed at the end of the request instead
* of lingering in the session.
*/
@Named("viewScopeLeakProbeBean")
@ViewScoped
public class ViewScopeLeakProbeBean implements Serializable
{
public static final AtomicInteger CREATED = new AtomicInteger();
public static final AtomicInteger DESTROYED = new AtomicInteger();

public static void reset()
{
CREATED.set(0);
DESTROYED.set(0);
}

@PostConstruct
public void created()
{
CREATED.incrementAndGet();
}

@PreDestroy
public void destroyed()
{
DESTROYED.incrementAndGet();
}

public String getValue()
{
return "view-scoped";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<!DOCTYPE html>
<html xmlns:h="jakarta.faces.html">
<h:head>
<title>Formless page referencing a @ViewScoped bean</title>
</h:head>
<h:body>
<!-- No h:form: this view's state is never saved, so it can never be restored. -->
<h:outputText value="#{viewScopeLeakProbeBean.value}"/>
</h:body>
</html>
Loading