com.helger.html.hc.special.HCSpecialNodeHandler Maven / Gradle / Ivy
/**
* Copyright (C) 2014-2016 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed 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 com.helger.html.hc.special;
import java.util.List;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.cache.AnnotationUsageCache;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.CommonsHashMap;
import com.helger.commons.collection.ext.CommonsLinkedHashSet;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.collection.ext.ICommonsMap;
import com.helger.commons.collection.ext.ICommonsOrderedSet;
import com.helger.commons.lang.ClassHelper;
import com.helger.commons.lang.GenericReflection;
import com.helger.commons.string.StringHelper;
import com.helger.html.annotation.OutOfBandNode;
import com.helger.html.hc.EHCNodeState;
import com.helger.html.hc.HCHelper;
import com.helger.html.hc.IHCHasChildrenMutable;
import com.helger.html.hc.IHCNode;
import com.helger.html.hc.config.HCSettings;
import com.helger.html.hc.config.IHCOnDocumentReadyProvider;
import com.helger.html.hc.html.IHCConditionalCommentNode;
import com.helger.html.hc.html.metadata.HCCSSNodeDetector;
import com.helger.html.hc.html.metadata.HCLink;
import com.helger.html.hc.html.metadata.HCStyle;
import com.helger.html.hc.html.script.HCJSNodeDetector;
import com.helger.html.hc.html.script.HCScriptFile;
import com.helger.html.hc.html.script.HCScriptInline;
import com.helger.html.hc.html.script.HCScriptInlineOnDocumentReady;
import com.helger.html.hc.html.script.IHCScriptInline;
import com.helger.html.js.CollectingJSCodeProvider;
import com.helger.html.resource.css.ICSSCodeProvider;
/**
* This class is used to handle the special nodes (JS and CSS, inline and
* reference).
*
* @author Philip Helger
*/
@NotThreadSafe
public final class HCSpecialNodeHandler
{
private static final Logger s_aLogger = LoggerFactory.getLogger (HCSpecialNodeHandler.class);
private static final AnnotationUsageCache s_aOOBNAnnotationCache = new AnnotationUsageCache (OutOfBandNode.class);
private static final AnnotationUsageCache s_aSNLMAnnotationCache = new AnnotationUsageCache (SpecialNodeListModifier.class);
private static final ICommonsMap s_aModifiers = new CommonsHashMap <> ();
@PresentForCodeCoverage
private static final HCSpecialNodeHandler s_aInstance = new HCSpecialNodeHandler ();
private HCSpecialNodeHandler ()
{}
/**
* Check if the passed node is an out-of-band node.
*
* @param aHCNode
* The node to be checked. May not be null
.
* @return true
if it is an out-of-band node, false
* if not.
*/
public static boolean isOutOfBandNode (@Nonnull final IHCNode aHCNode)
{
ValueEnforcer.notNull (aHCNode, "HCNode");
// Is the @OutOfBandNode annotation present?
if (s_aOOBNAnnotationCache.hasAnnotation (aHCNode))
return true;
// If it is a wrapped node, look into it
if (HCHelper.isWrappedNode (aHCNode))
return isOutOfBandNode (HCHelper.getUnwrappedNode (aHCNode));
// Not an out of band node
return false;
}
private static void _recursiveExtractAndRemoveOutOfBandNodes (@Nonnull final IHCNode aParentElement,
@Nonnull final List aTargetList,
@Nonnegative final int nLevel)
{
ValueEnforcer.notNull (aParentElement, "ParentElement");
if (aParentElement.hasChildren ())
{
final boolean bDebug = HCSettings.isOutOfBandDebuggingEnabled ();
int nNodeIndex = 0;
for (final IHCNode aChild : aParentElement.getAllChildren ())
{
if (bDebug)
s_aLogger.info (StringHelper.getRepeated (" ", nLevel) + ClassHelper.getClassLocalName (aChild.getClass ()));
if (isOutOfBandNode (aChild))
{
if (bDebug)
s_aLogger.info (StringHelper.getRepeated (" ", nLevel) + "=> is an OOB node!");
aTargetList.add (aChild);
if (aParentElement instanceof IHCHasChildrenMutable , ?>)
((IHCHasChildrenMutable , ?>) aParentElement).removeChild (nNodeIndex);
else
throw new IllegalStateException ("Cannot remove out-of-band node from " +
aParentElement +
" at index " +
nNodeIndex);
}
else
{
++nNodeIndex;
}
// Recurse deeper?
_recursiveExtractAndRemoveOutOfBandNodes (aChild, aTargetList, nLevel + 1);
}
}
}
/**
* Extract all out-of-band child nodes for the passed element. Must be called
* after the element is finished! All out-of-band nodes are detached from
* their parent so that the original node can be reused. Wrapped nodes where
* the inner node is an out of band node are also considered and removed.
*
* @param aParentElement
* The parent element to extract the nodes from. May not be
* null
.
* @param aTargetList
* The target list to be filled. May not be null
.
*/
public static void recursiveExtractAndRemoveOutOfBandNodes (@Nonnull final IHCNode aParentElement,
@Nonnull final List aTargetList)
{
ValueEnforcer.notNull (aParentElement, "ParentElement");
ValueEnforcer.notNull (aTargetList, "TargetList");
// Using HCUtils.iterateTree would be too tedious here
_recursiveExtractAndRemoveOutOfBandNodes (aParentElement, aTargetList, 0);
}
@Nonnull
public static Iterable extends IHCNode> applyModifiers (@Nonnull final Iterable extends IHCNode> aNodes)
{
ValueEnforcer.notNull (aNodes, "Nodes");
final ICommonsOrderedSet > aModifiersToApply = new CommonsLinkedHashSet <> ();
for (final IHCNode aNode : aNodes)
if (s_aSNLMAnnotationCache.hasAnnotation (aNode))
aModifiersToApply.add (aNode.getClass ().getAnnotation (SpecialNodeListModifier.class).value ());
if (aModifiersToApply.isEmpty ())
{
// No modifiers present - return as is
return aNodes;
}
// Ensure all modifiers are instantiated exactly once
for (final Class extends IHCSpecialNodeListModifier> aModifierClass : aModifiersToApply)
{
final String sClassName = aModifierClass.getName ();
if (!s_aModifiers.containsKey (sClassName))
{
final IHCSpecialNodeListModifier aModifier = GenericReflection.newInstance (aModifierClass);
if (aModifier == null)
s_aLogger.error ("Failed to instantiate IHCSpecialNodeListModifier implementation " + aModifierClass);
s_aModifiers.put (sClassName, aModifier);
}
}
// Apply all modifiers
ICommonsList extends IHCNode> ret = new CommonsArrayList <> (aNodes);
for (final Class extends IHCSpecialNodeListModifier> aModifierClass : aModifiersToApply)
{
final IHCSpecialNodeListModifier aModifier = s_aModifiers.get (aModifierClass.getName ());
if (aModifier != null)
{
// Invocation successful
ret = aModifier.modifySpecialNodes (ret);
}
}
return ret;
}
/**
* Merge all inline CSS and JS elements contained in the source nodes into one
* script elements
*
* @param aNodes
* Source list of nodes. May not be null
.
* @param bKeepOnDocumentReady
* if true
than all combined document.ready() scripts are
* kept as document.ready() scripts. If false
than all
* document.ready() scripts are converted to regular scripts and are
* executed after all other scripts. For AJAX calls, this should be
* false
.
* @return Target list. JS and CSS and other nodes are mixed. Inline JS and
* CSS that comes before files, is first. Than come the CSS and JS
* external as well as other elements. Finally the inline JS and CSS
* nodes to be emitted after the files are contained. So the resulting
* order is at it should be except that JS and CSS and other nodes are
* mixed.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsList getMergedInlineCSSAndJSNodes (@Nonnull final Iterable extends IHCNode> aNodes,
final boolean bKeepOnDocumentReady)
{
// Default to the global "on document ready" provider
return getMergedInlineCSSAndJSNodes (aNodes,
bKeepOnDocumentReady ? HCSettings.getOnDocumentReadyProvider () : null);
}
/**
* Merge all inline CSS and JS elements contained in the source nodes into one
* script elements
*
* @param aNodes
* Source list of nodes. May not be null
.
* @param aOnDocumentReadyProvider
* if not null
than all combined document.ready() scripts
* are kept as document.ready() scripts using this provider. If
* null
than all document.ready() scripts are converted to
* regular scripts and are executed after all other scripts. For AJAX
* calls, this should be null
as there is no
* "document ready" callback - alternatively you can provide a custom
* "on document ready" provider.
* @return Target list. JS and CSS and other nodes are mixed. Inline JS and
* CSS that comes before files, is first. Than come the CSS and JS
* external as well as other elements. Finally the inline JS and CSS
* nodes to be emitted after the files are contained. So the resulting
* order is at it should be except that JS and CSS and other nodes are
* mixed.
*/
@Nonnull
@ReturnsMutableCopy
public static ICommonsList getMergedInlineCSSAndJSNodes (@Nonnull final Iterable extends IHCNode> aNodes,
@Nullable final IHCOnDocumentReadyProvider aOnDocumentReadyProvider)
{
ValueEnforcer.notNull (aNodes, "Nodes");
// Apply all modifiers
final Iterable extends IHCNode> aRealSpecialNodes = applyModifiers (aNodes);
// Do standard aggregations of CSS and JS
final ICommonsList ret = new CommonsArrayList <> ();
final CollectingJSCodeProvider aJSOnDocumentReadyBefore = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSOnDocumentReadyAfter = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSInlineBefore = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSInlineAfter = new CollectingJSCodeProvider ();
final InlineCSSList aCSSInlineBefore = new InlineCSSList ();
final InlineCSSList aCSSInlineAfter = new InlineCSSList ();
for (final IHCNode aNode : aRealSpecialNodes)
{
// Note: do not unwrap the node, because it is not allowed to merge JS/CSS
// with a conditional comment with JS/CSS without a conditional comment!
if (HCJSNodeDetector.isDirectJSInlineNode (aNode))
{
// Check HCScriptInlineOnDocumentReady first, because it is a subclass
// of IHCScriptInline
if (aNode instanceof HCScriptInlineOnDocumentReady)
{
// Inline JS
final HCScriptInlineOnDocumentReady aScript = (HCScriptInlineOnDocumentReady) aNode;
(aScript.isEmitAfterFiles () ? aJSOnDocumentReadyAfter
: aJSOnDocumentReadyBefore).appendFlattened (aScript.getOnDocumentReadyCode ());
}
else
{
// Inline JS
final IHCScriptInline > aScript = (IHCScriptInline >) aNode;
(aScript.isEmitAfterFiles () ? aJSInlineAfter
: aJSInlineBefore).appendFlattened (aScript.getJSCodeProvider ());
}
}
else
if (HCCSSNodeDetector.isDirectCSSInlineNode (aNode))
{
// Inline CSS
final HCStyle aStyle = (HCStyle) aNode;
(aStyle.isEmitAfterFiles () ? aCSSInlineAfter : aCSSInlineBefore).addInlineCSS (aStyle.getMedia (),
aStyle.getStyleContent ());
}
else
{
// HCLink
// HCScriptFile
// HCConditionalCommentNode
if (!(aNode instanceof HCLink) &&
!(aNode instanceof HCScriptFile) &&
!(aNode instanceof IHCConditionalCommentNode))
s_aLogger.warn ("Found unexpected node to merge inline CSS/JS: " + aNode);
// Add always!
// These nodes are either file based nodes ot conditional comment
// nodes
ret.add (aNode);
}
}
// on-document-ready JS always as last inline JS!
if (!aJSOnDocumentReadyBefore.isEmpty ())
if (aOnDocumentReadyProvider != null)
aJSInlineBefore.append (aOnDocumentReadyProvider.createOnDocumentReady (aJSOnDocumentReadyBefore));
else
aJSInlineBefore.append (aJSOnDocumentReadyBefore);
if (!aJSOnDocumentReadyAfter.isEmpty ())
if (aOnDocumentReadyProvider != null)
aJSInlineAfter.append (aOnDocumentReadyProvider.createOnDocumentReady (aJSOnDocumentReadyAfter));
else
aJSInlineAfter.append (aJSOnDocumentReadyAfter);
// Finally add the inline JS
if (!aJSInlineBefore.isEmpty ())
{
// Add at the beginning
final HCScriptInline aScript = new HCScriptInline (aJSInlineBefore).setEmitAfterFiles (false);
aScript.internalSetNodeState (EHCNodeState.RESOURCES_REGISTERED);
ret.add (0, aScript);
}
if (!aJSInlineAfter.isEmpty ())
{
// Add at the end
final HCScriptInline aScript = new HCScriptInline (aJSInlineAfter).setEmitAfterFiles (true);
aScript.internalSetNodeState (EHCNodeState.RESOURCES_REGISTERED);
ret.add (aScript);
}
// Add all merged inline CSSs grouped by their media list
if (aCSSInlineBefore.isNotEmpty ())
{
// Add at the beginning
int nIndex = 0;
for (final ICSSCodeProvider aEntry : aCSSInlineBefore.getAll ())
{
final HCStyle aStyle = new HCStyle (aEntry.getCSSCode ()).setMedia (aEntry.getMediaList ())
.setEmitAfterFiles (false);
aStyle.internalSetNodeState (EHCNodeState.RESOURCES_REGISTERED);
ret.add (nIndex, aStyle);
++nIndex;
}
}
if (aCSSInlineAfter.isNotEmpty ())
{
// Add at the end
for (final ICSSCodeProvider aEntry : aCSSInlineAfter.getAll ())
{
final HCStyle aStyle = new HCStyle (aEntry.getCSSCode ()).setMedia (aEntry.getMediaList ())
.setEmitAfterFiles (true);
aStyle.internalSetNodeState (EHCNodeState.RESOURCES_REGISTERED);
ret.add (aStyle);
}
}
return ret;
}
@Nonnull
@ReturnsMutableCopy
public static ICommonsList extractSpecialNodes (@Nonnull final Iterable extends IHCNode> aNodes,
@Nonnull final AbstractHCSpecialNodes > aSpecialNodes)
{
ValueEnforcer.notNull (aNodes, "Nodes");
ValueEnforcer.notNull (aSpecialNodes, "SpecialNodes");
final ICommonsList ret = new CommonsArrayList <> ();
for (final IHCNode aNode : aNodes)
{
if (HCCSSNodeDetector.isDirectCSSFileNode (aNode))
{
final HCLink aLink = (HCLink) aNode;
if (aLink.getHref () != null)
{
// Use the default charset here :|
aSpecialNodes.addExternalCSS (aLink.getMedia (), aLink.getHref ().getAsStringWithEncodedParameters ());
}
}
else
if (HCCSSNodeDetector.isDirectCSSInlineNode (aNode))
{
final HCStyle aStyle = (HCStyle) aNode;
if (aStyle.isEmitAfterFiles ())
aSpecialNodes.addInlineCSSAfterExternal (aStyle.getMedia (), aStyle.getStyleContent ());
else
aSpecialNodes.addInlineCSSBeforeExternal (aStyle.getMedia (), aStyle.getStyleContent ());
}
else
if (HCJSNodeDetector.isDirectJSFileNode (aNode))
{
final HCScriptFile aScriptFile = (HCScriptFile) aNode;
if (aScriptFile.getSrc () != null)
{
// Use the default charset here :|
aSpecialNodes.addExternalJS (aScriptFile.getSrc ().getAsStringWithEncodedParameters ());
}
}
else
if (HCJSNodeDetector.isDirectJSInlineNode (aNode))
{
final IHCScriptInline > aScript = (IHCScriptInline >) aNode;
if (aScript.isEmitAfterFiles ())
aSpecialNodes.addInlineJSAfterExternal (aScript.getJSCodeProvider ());
else
aSpecialNodes.addInlineJSBeforeExternal (aScript.getJSCodeProvider ());
}
else
{
// Neither JS nor CSS node - so maybe a conditional comment node
ret.add (aNode);
}
}
return ret;
}
/**
* Extract all out-of-band nodes of the source node, merge JS and CSS and
* finally extract all special nodes into the passed object.
*
* @param aNode
* Source node. May not be null
.
* @param aSpecialNodes
* Target special node object to be filled. May not be
* null
.
* @param bKeepOnDocumentReady
* if true
than all combined document.ready() scripts are
* kept as document.ready() scripts. If false
than all
* document.ready() scripts are converted to regular scripts and are
* executed after all other scripts. For AJAX calls, this should be
* false
.
*/
public static void extractSpecialContent (@Nonnull final IHCNode aNode,
@Nonnull final AbstractHCSpecialNodes > aSpecialNodes,
final boolean bKeepOnDocumentReady)
{
extractSpecialContent (aNode,
aSpecialNodes,
bKeepOnDocumentReady ? HCSettings.getOnDocumentReadyProvider () : null);
}
/**
* Extract all out-of-band nodes of the source node, merge JS and CSS and
* finally extract all special nodes into the passed object.
*
* @param aNode
* Source node. May not be null
.
* @param aSpecialNodes
* Target special node object to be filled. May not be
* null
.
* @param aOnDocumentReadyProvider
* if not null
than all combined document.ready() scripts
* are kept as document.ready() scripts using this provider. If
* null
than all document.ready() scripts are converted to
* regular scripts and are executed after all other scripts. For AJAX
* calls, this should be null
as there is no
* "document ready" callback - alternatively you can provide a custom
* "on document ready" provider.
*/
public static void extractSpecialContent (@Nonnull final IHCNode aNode,
@Nonnull final AbstractHCSpecialNodes > aSpecialNodes,
@Nullable final IHCOnDocumentReadyProvider aOnDocumentReadyProvider)
{
ValueEnforcer.notNull (aNode, "Node");
ValueEnforcer.notNull (aSpecialNodes, "SpecialNodes");
// Extract all out of band nodes from the passed node
ICommonsList aExtractedOutOfBandNodes = new CommonsArrayList <> ();
recursiveExtractAndRemoveOutOfBandNodes (aNode, aExtractedOutOfBandNodes);
// Merge JS/CSS nodes - replace list content
aExtractedOutOfBandNodes = getMergedInlineCSSAndJSNodes (aExtractedOutOfBandNodes, aOnDocumentReadyProvider);
// Extract the special nodes into the provided object
aExtractedOutOfBandNodes = extractSpecialNodes (aExtractedOutOfBandNodes, aSpecialNodes);
// Now the aExtractedOutOfBandNodes list must be empty - otherwise we have
// an internal inconsistency
if (aExtractedOutOfBandNodes.isNotEmpty ())
throw new IllegalStateException ("Out-of-band nodes are left after merging and extraction: " +
aExtractedOutOfBandNodes);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy