blob: 8f9e6a0955f979a4303f14427ea97412a719c456 [file] [log] [blame]
/*
* 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.renderkit.html;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseStream;
import jakarta.faces.context.ResponseWriter;
import jakarta.faces.render.ClientBehaviorRenderer;
import jakarta.faces.render.RenderKit;
import jakarta.faces.render.Renderer;
import jakarta.faces.render.RendererWrapper;
import jakarta.faces.render.ResponseStateManager;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFRenderKit;
import org.apache.myfaces.renderkit.LazyRenderKit;
import org.apache.myfaces.config.MyfacesConfig;
import org.apache.myfaces.renderkit.ContentTypeUtils;
import org.apache.myfaces.core.api.shared.lang.Assert;
import org.apache.myfaces.util.lang.ClassUtils;
/**
* @author Manfred Geiler (latest modification by $Author$)
* @version $Revision$ $Date$
*/
@JSFRenderKit(renderKitId = "HTML_BASIC")
public class HtmlRenderKitImpl extends RenderKit implements LazyRenderKit
{
private static final Logger log = Logger.getLogger(HtmlRenderKitImpl.class.getName());
// ~ Instance fields ----------------------------------------------------------------------------
private Map<String, Map<String, Renderer>> _renderers;
private ResponseStateManager _responseStateManager;
private Map<String, ClientBehaviorRenderer> _clientBehaviorRenderers;
private MyfacesConfig myfacesConfig;
// ~ Constructors -------------------------------------------------------------------------------
public HtmlRenderKitImpl()
{
_renderers = new ConcurrentHashMap<>(64, 0.75f, 1);
_responseStateManager = new HtmlResponseStateManager();
_clientBehaviorRenderers = new HashMap<>();
myfacesConfig = MyfacesConfig.getCurrentInstance();
}
// ~ Methods ------------------------------------------------------------------------------------
@Override
public void addClientBehaviorRenderer(String type, ClientBehaviorRenderer renderer)
{
Assert.notNull(type, "type");
Assert.notNull(renderer, "renderer");
_clientBehaviorRenderers.put(type, renderer);
}
@Override
public ClientBehaviorRenderer getClientBehaviorRenderer(String type)
{
Assert.notNull(type, "type");
return _clientBehaviorRenderers.get(type);
}
@Override
public Iterator<String> getClientBehaviorRendererTypes()
{
return _clientBehaviorRenderers.keySet().iterator();
}
@Override
public Renderer getRenderer(String componentFamily, String rendererType)
{
Assert.notNull(componentFamily, "componentFamily");
Assert.notNull(rendererType, "rendererType");
Map <String,Renderer> familyRendererMap = _renderers.get(componentFamily);
Renderer renderer = null;
if (familyRendererMap != null)
{
renderer = familyRendererMap.get(rendererType);
}
if (renderer == null)
{
log.warning("Unsupported component-family/renderer-type: " + componentFamily + '/' + rendererType);
}
if (renderer instanceof LazyRendererWrapper)
{
renderer = ((LazyRendererWrapper)renderer).getWrapped();
familyRendererMap.put(rendererType, renderer);
}
return renderer;
}
@Override
public void addRenderer(String componentFamily, String rendererType, Renderer renderer)
{
Assert.notNull(componentFamily, "componentFamily");
Assert.notNull(rendererType, "rendererType");
Assert.notNull(renderer, "renderer");
_put(componentFamily, rendererType, renderer);
if (log.isLoggable(Level.FINEST))
{
log.finest("add Renderer family = " + componentFamily + " rendererType = " + rendererType
+ " renderer class = " + renderer.getClass().getName());
}
}
@Override
public void addRenderer(String componentFamily, String rendererType, String rendererClass)
{
Assert.notNull(componentFamily, "componentFamily");
Assert.notNull(rendererType, "rendererType");
Assert.notNull(rendererClass, "rendererClass");
_put(componentFamily, rendererType, new LazyRendererWrapper(rendererClass));
if (log.isLoggable(Level.FINEST))
{
log.finest("add Renderer family = " + componentFamily + " rendererType = " + rendererType
+ " renderer class = " + rendererClass);
}
}
/**
* Put the renderer on the double map
*
* @param componentFamily
* @param rendererType
* @param renderer
*/
synchronized private void _put(String componentFamily, String rendererType, Renderer renderer)
{
Map <String,Renderer> familyRendererMap = _renderers.get(componentFamily);
if (familyRendererMap == null)
{
familyRendererMap = new ConcurrentHashMap<>(8, 0.75f, 1);
_renderers.put(componentFamily, familyRendererMap);
}
else
{
if (familyRendererMap.get(rendererType) != null)
{
// this is not necessarily an error, but users do need to be
// very careful about jar processing order when overriding
// some component's renderer with an alternate renderer.
log.fine("Overwriting renderer with family = " + componentFamily +
" rendererType = " + rendererType +
" renderer class = " + renderer.getClass().getName());
}
}
familyRendererMap.put(rendererType, renderer);
}
@Override
public ResponseStateManager getResponseStateManager()
{
return _responseStateManager;
}
/**
* @since JSF 2.0
*/
@Override
public Iterator<String> getComponentFamilies()
{
//return _families.keySet().iterator();
return _renderers.keySet().iterator();
}
/**
* @since JSF 2.0
*/
@Override
public Iterator<String> getRendererTypes(String componentFamily)
{
//Return an Iterator over the renderer-type entries for the given component-family.
Map<String, Renderer> map = _renderers.get(componentFamily);
if (map != null)
{
return map.keySet().iterator();
}
//If the specified componentFamily is not known to this RenderKit implementation, return an empty Iterator
return Collections.<String>emptySet().iterator();
}
@Override
public ResponseWriter createResponseWriter(Writer writer, String contentTypeListString, String characterEncoding)
{
FacesContext facesContext = FacesContext.getCurrentInstance();
String selectedContentType = null;
String writerContentType = null;
boolean isAjaxRequest = facesContext.getPartialViewContext().isAjaxRequest();
String contentTypeListStringFromAccept = null;
// To detect the right contentType, we need to check if the request is an ajax request or not.
// If it is an ajax request, HTTP Accept header content type will be set for the ajax itself, which
// is application/xml or text/xml. In that case, there are two response writers
// (PartialResponseWriterImpl and HtmlResponseWriterImpl),
//1. if there is a passed contentTypeListString, it takes precedence over accept header
if (contentTypeListString != null)
{
selectedContentType = ContentTypeUtils.chooseWriterContentType(contentTypeListString,
ContentTypeUtils.HTML_ALLOWED_CONTENT_TYPES,
isAjaxRequest ? ContentTypeUtils.AJAX_XHTML_ALLOWED_CONTENT_TYPES :
ContentTypeUtils.XHTML_ALLOWED_CONTENT_TYPES);
}
//2. If no selectedContentType
// try to derive it from accept header
if (selectedContentType == null)
{
contentTypeListStringFromAccept =
ContentTypeUtils.getContentTypeFromAcceptHeader(facesContext);
if (contentTypeListStringFromAccept != null)
{
selectedContentType = ContentTypeUtils.chooseWriterContentType(contentTypeListStringFromAccept,
ContentTypeUtils.HTML_ALLOWED_CONTENT_TYPES,
isAjaxRequest ? ContentTypeUtils.AJAX_XHTML_ALLOWED_CONTENT_TYPES :
ContentTypeUtils.XHTML_ALLOWED_CONTENT_TYPES);
}
}
//3. if no selectedContentType was derived, set default from the param
if (selectedContentType == null)
{
if (contentTypeListString == null && contentTypeListStringFromAccept == null)
{
//If no contentTypeList, return the default
selectedContentType = myfacesConfig.getDefaultResponseWriterContentTypeMode();
}
else
{
// If a contentTypeList was passed and we don't have direct matches, we still need
// to check if */* is found and if that so return the default, otherwise throw
// exception.
if (contentTypeListString != null)
{
String[] contentTypes = ContentTypeUtils.splitContentTypeListString(contentTypeListString);
if (ContentTypeUtils.containsContentType(ContentTypeUtils.ANY_CONTENT_TYPE, contentTypes))
{
selectedContentType = myfacesConfig.getDefaultResponseWriterContentTypeMode();
}
}
if (selectedContentType == null)
{
if (contentTypeListStringFromAccept != null)
{
String[] contentTypes = ContentTypeUtils.splitContentTypeListString(
contentTypeListStringFromAccept);
if (ContentTypeUtils.containsContentType(ContentTypeUtils.ANY_CONTENT_TYPE, contentTypes))
{
selectedContentType = myfacesConfig.getDefaultResponseWriterContentTypeMode();
}
}
else if (isAjaxRequest)
{
// If is an ajax request, contentTypeListStringFromAccept == null and
// contentTypeListString != null, contentTypeListString should not be taken
// into account, because the final content type in this case is for PartialResponseWriter
// implementation. In this case rfc2616-sec14 takes precedence:
//
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// 14.1 Accept
// If no Accept header field is present, then it is assumed that the client
// accepts all media types.
selectedContentType = myfacesConfig.getDefaultResponseWriterContentTypeMode();
}
if (selectedContentType == null)
{
// Note this case falls when contentTypeListStringFromAccept == null and
// contentTypeListString != null, but since this not an ajax request,
// contentTypeListString should be taken strictly and throw IllegalArgumentException
throw new IllegalArgumentException(
"ContentTypeList does not contain a supported content type: "
+ ((contentTypeListString != null) ?
contentTypeListString : contentTypeListStringFromAccept) );
}
}
}
}
if (isAjaxRequest)
{
// If HTTP Accept header has application/xml or text/xml, that does not means the writer
// content type mode should be set to application/xhtml+xml.
writerContentType = selectedContentType.contains(ContentTypeUtils.XHTML_CONTENT_TYPE) ?
ContentTypeUtils.XHTML_CONTENT_TYPE : ContentTypeUtils.HTML_CONTENT_TYPE;
}
else
{
writerContentType = ContentTypeUtils.isXHTMLContentType(selectedContentType) ?
ContentTypeUtils.XHTML_CONTENT_TYPE : ContentTypeUtils.HTML_CONTENT_TYPE;
}
if (characterEncoding == null)
{
characterEncoding = ContentTypeUtils.DEFAULT_CHAR_ENCODING;
}
if (myfacesConfig.isEarlyFlushEnabled())
{
return new EarlyFlushHtmlResponseWriterImpl(writer, selectedContentType, characterEncoding,
myfacesConfig.isWrapScriptContentWithXmlCommentTag(), writerContentType);
}
else
{
return new HtmlResponseWriterImpl(writer, selectedContentType, characterEncoding,
myfacesConfig.isWrapScriptContentWithXmlCommentTag(), writerContentType);
}
}
@Override
public ResponseStream createResponseStream(OutputStream outputStream)
{
return new MyFacesResponseStream(outputStream);
}
private static class MyFacesResponseStream extends ResponseStream
{
private OutputStream output;
public MyFacesResponseStream(OutputStream output)
{
this.output = output;
}
@Override
public void write(int b) throws IOException
{
output.write(b);
}
@Override
public void write(byte b[]) throws IOException
{
output.write(b);
}
@Override
public void write(byte b[], int off, int len) throws IOException
{
output.write(b, off, len);
}
@Override
public void flush() throws IOException
{
output.flush();
}
@Override
public void close() throws IOException
{
output.close();
}
}
private static class LazyRendererWrapper extends RendererWrapper
{
private String rendererClass;
private Renderer delegate;
public LazyRendererWrapper(String rendererClass)
{
this.rendererClass = rendererClass;
}
@Override
public Renderer getWrapped()
{
if (delegate == null)
{
delegate = (Renderer) ClassUtils.newInstance(
ClassUtils.simpleClassForName(rendererClass));
}
return delegate;
}
}
}