
com.phloc.html.hc.utils.HCSpecialNodeHandler Maven / Gradle / Ivy
/**
* Copyright (C) 2006-2015 phloc systems
* http://www.phloc.com
* office[at]phloc[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.phloc.html.hc.utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
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.phloc.commons.ValueEnforcer;
import com.phloc.commons.annotations.PresentForCodeCoverage;
import com.phloc.commons.annotations.ReturnsMutableCopy;
import com.phloc.commons.cache.AnnotationUsageCache;
import com.phloc.commons.collections.ContainerHelper;
import com.phloc.commons.lang.CGStringHelper;
import com.phloc.commons.lang.GenericReflection;
import com.phloc.commons.string.StringHelper;
import com.phloc.html.annotations.OutOfBandNode;
import com.phloc.html.hc.IHCCSSNode;
import com.phloc.html.hc.IHCHasChildren;
import com.phloc.html.hc.IHCJSNode;
import com.phloc.html.hc.IHCNode;
import com.phloc.html.hc.IHCNodeWithChildren;
import com.phloc.html.hc.html.HCLink;
import com.phloc.html.hc.html.HCScript;
import com.phloc.html.hc.html.HCScriptFile;
import com.phloc.html.hc.html.HCScriptOnDocumentReady;
import com.phloc.html.hc.html.HCStyle;
import com.phloc.html.hc.htmlext.HCUtils;
import com.phloc.html.hc.impl.HCConditionalCommentNode;
import com.phloc.html.hc.impl.HCNodeList;
import com.phloc.html.js.builder.jquery.JQuery;
import com.phloc.html.js.provider.CollectingJSCodeProvider;
/**
* 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 AtomicBoolean s_aOOBDebugging = new AtomicBoolean (false);
@PresentForCodeCoverage
@SuppressWarnings ("unused")
private static final HCSpecialNodeHandler s_aInstance = new HCSpecialNodeHandler ();
private HCSpecialNodeHandler ()
{}
public static boolean isOutOfBandDebuggingEnabled ()
{
return s_aOOBDebugging.get ();
}
public static void setOutOfBandDebuggingEnabled (final boolean bEnabled)
{
s_aOOBDebugging.set (bEnabled);
}
/**
* Check if the passed node is a CSS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link IHCCSSNode} (and
* not a special case).
*/
public static boolean isCSSNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectCSSNode (aUnwrappedNode);
}
/**
* Check if the passed node is a CSS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link IHCCSSNode} (and
* not a special case).
*/
public static boolean isDirectCSSNode (@Nullable final IHCNode aNode)
{
// Direct CSS node?
if (aNode instanceof IHCCSSNode)
{
// Special case
if (aNode instanceof HCLink && !((HCLink) aNode).isCSSLink ())
return false;
return true;
}
return false;
}
/**
* Check if the passed node is an inline CSS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCStyle}.
*/
public static boolean isCSSInlineNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectCSSInlineNode (aUnwrappedNode);
}
/**
* Check if the passed node is an inline CSS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCStyle}.
*/
public static boolean isDirectCSSInlineNode (@Nullable final IHCNode aNode)
{
// Inline CSS node?
return aNode instanceof HCStyle;
}
/**
* Check if the passed node is a file CSS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCStyle}.
*/
public static boolean isCSSFileNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectCSSFileNode (aUnwrappedNode);
}
/**
* Check if the passed node is a file CSS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCStyle}.
*/
public static boolean isDirectCSSFileNode (@Nullable final IHCNode aNode)
{
// File CSS node?
return aNode instanceof HCLink && ((HCLink) aNode).isCSSLink ();
}
/**
* Check if the passed node is a JS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link IHCJSNode}.
*/
public static boolean isJSNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectJSNode (aUnwrappedNode);
}
/**
* Check if the passed node is a JS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link IHCJSNode}.
*/
public static boolean isDirectJSNode (@Nullable final IHCNode aNode)
{
// Direct JS node?
return aNode instanceof IHCJSNode;
}
/**
* Check if the passed node is an inline JS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCScript}.
*/
public static boolean isJSInlineNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectJSInlineNode (aUnwrappedNode);
}
/**
* Check if the passed node is an inline JS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCScript}.
*/
public static boolean isDirectJSInlineNode (@Nullable final IHCNode aNode)
{
// Inline JS node?
return aNode instanceof HCScript;
}
/**
* Check if the passed node is a file JS node after unwrapping.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCScriptFile}.
*/
public static boolean isJSFileNode (@Nullable final IHCNode aNode)
{
final IHCNode aUnwrappedNode = HCUtils.getUnwrappedNode (aNode);
return isDirectJSFileNode (aUnwrappedNode);
}
/**
* Check if the passed node is a file JS node.
*
* @param aNode
* The node to be checked - may be null
.
* @return true
if the node implements {@link HCScriptFile}.
*/
public static boolean isDirectJSFileNode (@Nullable final IHCNode aNode)
{
// File JS node?
return aNode instanceof HCScriptFile;
}
/**
* 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 (HCUtils.isWrappedNode (aHCNode))
return isOutOfBandNode (HCUtils.getUnwrappedNode (aHCNode));
// Not an out of band node
return false;
}
private static void _recursiveExtractAndRemoveOutOfBandNodes (@Nonnull final IHCHasChildren aParentElement,
@Nonnull final List aTargetList,
@Nonnegative final int nLevel)
{
ValueEnforcer.notNull (aParentElement, "ParentElement");
if (aParentElement.hasChildren ())
{
final boolean bDebug = isOutOfBandDebuggingEnabled ();
int nNodeIndex = 0;
for (final IHCNode aChild : aParentElement.getChildren ())
{
if (bDebug)
s_aLogger.info (StringHelper.getRepeated (" ", nLevel) +
CGStringHelper.getClassLocalName (aChild.getClass ()));
if (isOutOfBandNode (aChild))
{
if (bDebug)
s_aLogger.info (StringHelper.getRepeated (" ", nLevel) + "=> is an OOB node!");
aTargetList.add (aChild);
if (aParentElement instanceof IHCNodeWithChildren >)
((IHCNodeWithChildren >) aParentElement).removeChild (nNodeIndex);
else
throw new IllegalStateException ("Cannot have out-of-band nodes at " + aParentElement);
}
else
{
++nNodeIndex;
}
// Recurse deeper?
if (aChild instanceof IHCHasChildren)
_recursiveExtractAndRemoveOutOfBandNodes ((IHCHasChildren) aChild, aTargetList, nLevel + 1);
}
}
}
/**
* Extract all out-of-band child nodes for the passed element. Ensure to call
* {@link com.phloc.html.hc.IHCNode#beforeConvertToNode(com.phloc.html.hc.conversion.IHCConversionSettingsToNode)}
* before calling this method! All out-of-band nodes are detached from their
* parent so that the original node can be reused.
*
* @param aParentElement
* The parent element to extract the nodes from. May not be
* null
.
* @return The list with out-of-band nodes. Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public static List recursiveExtractAndRemoveOutOfBandNodes (@Nonnull final IHCHasChildren aParentElement)
{
ValueEnforcer.notNull (aParentElement, "ParentElement");
final List aTargetList = new ArrayList ();
// Using HCUtils.iterateTree would be too tedious here
_recursiveExtractAndRemoveOutOfBandNodes (aParentElement, aTargetList, 0);
return aTargetList;
}
private static final Map s_aModifiers = new HashMap ();
@Nonnull
private static Iterable extends IHCNode> _applyModifiers (@Nonnull final Iterable extends IHCNode> aNodes)
{
final Set > aModifiersToApply = new LinkedHashSet > ();
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
for (final Class extends IHCSpecialNodeListModifier> aModifierClass : aModifiersToApply)
{
final String sClassName = aModifierClass.getName ();
if (!s_aModifiers.containsKey (sClassName))
s_aModifiers.put (sClassName, GenericReflection.newInstance (aModifierClass));
}
// Apply all modifiers
List extends IHCNode> ret = ContainerHelper.newList (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. It contains all non-script nodes and at last one JS
* inline node (HCScript).
*/
@Nonnull
@ReturnsMutableCopy
public static List getMergedInlineCSSAndJSNodes (@Nonnull final Iterable extends IHCNode> aNodes,
final boolean bKeepOnDocumentReady)
{
ValueEnforcer.notNull (aNodes, "Nodes");
// Apply all modifiers
final Iterable extends IHCNode> aRealSpecialNodes = _applyModifiers (aNodes);
// Do standard aggregations of CSS and JS
final List ret = new ArrayList ();
final CollectingJSCodeProvider aJSOnDocumentReadyBefore = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSOnDocumentReadyAfter = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSInlineBefore = new CollectingJSCodeProvider ();
final CollectingJSCodeProvider aJSInlineAfter = new CollectingJSCodeProvider ();
final StringBuilder aCSSInlineBefore = new StringBuilder ();
final StringBuilder aCSSInlineAfter = new StringBuilder ();
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!
// Check HCScriptOnDocumentReady first, because it is a subclass of
// HCScript
if (aNode instanceof HCScriptOnDocumentReady)
{
final HCScriptOnDocumentReady aScript = (HCScriptOnDocumentReady) aNode;
(aScript.isEmitAfterFiles () ? aJSOnDocumentReadyAfter : aJSOnDocumentReadyBefore).appendFlattened (aScript.getOnDocumentReadyCode ());
}
else
if (aNode instanceof HCScript)
{
final HCScript aScript = (HCScript) aNode;
(aScript.isEmitAfterFiles () ? aJSInlineAfter : aJSInlineBefore).appendFlattened (aScript.getJSCodeProvider ());
}
else
if (aNode instanceof HCStyle && ((HCStyle) aNode).hasNoMediaOrAll ())
{
// Merge only inline CSS nodes, that are media-independent
final HCStyle aStyle = (HCStyle) aNode;
(aStyle.isEmitAfterFiles () ? aCSSInlineAfter : aCSSInlineBefore).append (aStyle.getStyleContent ());
}
else
{
// HCLink
// HCScriptFile
// HCConditionalCommentNode
if (!(aNode instanceof HCLink) &&
!(aNode instanceof HCScriptFile) &&
!(aNode instanceof HCConditionalCommentNode))
s_aLogger.warn ("Found unexpected node to merge inline CSS/JS: " + aNode);
// Add always!
ret.add (aNode);
}
}
// on-document-ready JS always as last inline JS!
if (!aJSOnDocumentReadyBefore.isEmpty ())
if (bKeepOnDocumentReady)
aJSInlineBefore.append (JQuery.onDocumentReady (aJSOnDocumentReadyBefore));
else
aJSInlineBefore.append (aJSOnDocumentReadyBefore);
if (!aJSOnDocumentReadyAfter.isEmpty ())
if (bKeepOnDocumentReady)
aJSInlineAfter.append (JQuery.onDocumentReady (aJSOnDocumentReadyAfter));
else
aJSInlineAfter.append (aJSOnDocumentReadyAfter);
// Finally add the inline JS
if (!aJSInlineBefore.isEmpty ())
ret.add (new HCScript (aJSInlineBefore).setEmitAfterFiles (false));
if (!aJSInlineAfter.isEmpty ())
ret.add (new HCScript (aJSInlineAfter).setEmitAfterFiles (true));
// Add all merged inline CSSs
if (aCSSInlineBefore.length () > 0)
ret.add (new HCStyle (aCSSInlineBefore.toString ()).setEmitAfterFiles (false));
if (aCSSInlineAfter.length () > 0)
ret.add (new HCStyle (aCSSInlineAfter.toString ()).setEmitAfterFiles (true));
return ret;
}
@Nonnull
@ReturnsMutableCopy
public static List getWithoutSpecialNodes (@Nonnull final Iterable extends IHCNode> aNodes,
@Nonnull final AbstractHCSpecialNodes > aSpecialNodes)
{
ValueEnforcer.notNull (aNodes, "Nodes");
ValueEnforcer.notNull (aSpecialNodes, "SpecialNodes");
final List ret = new ArrayList ();
for (final IHCNode aNode : aNodes)
{
if (isDirectCSSFileNode (aNode))
{
aSpecialNodes.addExternalCSS (((HCLink) aNode).getHrefString ());
}
else
if (isDirectCSSInlineNode (aNode))
{
aSpecialNodes.addInlineCSS (((HCStyle) aNode).getStyleContent ());
}
else
if (isDirectJSFileNode (aNode))
{
aSpecialNodes.addExternalJS (((HCScriptFile) aNode).getSrcString ());
}
else
if (isDirectJSInlineNode (aNode))
{
aSpecialNodes.addInlineJS (((HCScript) aNode).getJSCodeProvider ());
}
else
{
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
.
* @return A node list with all remaining (non-special) nodes. Never
* null
.
*/
@Nonnull
public static HCNodeList extractSpecialContent (@Nonnull final IHCHasChildren aNode,
@Nonnull final AbstractHCSpecialNodes > aSpecialNodes,
final boolean bKeepOnDocumentReady)
{
ValueEnforcer.notNull (aNode, "Node");
ValueEnforcer.notNull (aSpecialNodes, "SpecialNodes");
// Extract all out of band nodes from the passed node
List aExtractedOutOfBandNodes = recursiveExtractAndRemoveOutOfBandNodes (aNode);
// Merge JS/CSS nodes
aExtractedOutOfBandNodes = getMergedInlineCSSAndJSNodes (aExtractedOutOfBandNodes, bKeepOnDocumentReady);
// Extract the special nodes into the provided object
aExtractedOutOfBandNodes = getWithoutSpecialNodes (aExtractedOutOfBandNodes, aSpecialNodes);
// Now the aExtractedOutOfBandNodes list should be empty - otherwise we have
// an internal inconsistency (see the warning below)
if (!aExtractedOutOfBandNodes.isEmpty ())
s_aLogger.warn ("Out-of-band nodes are left after merging and extraction: " + aExtractedOutOfBandNodes);
// Add the content without the out-of-band nodes
final HCNodeList ret = HCNodeList.create (aNode);
// And to be sure, add all remaining out-of-band nodes at the end so that no
// node will get lost!
ret.addChildren (aExtractedOutOfBandNodes);
return ret;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy