From af980e39a19870d3746eaae3ba1f8c09d37d3d00 Mon Sep 17 00:00:00 2001 From: Jack Smith Date: Wed, 4 Oct 2023 10:34:24 -0400 Subject: [PATCH] Split aliased and flat importing strategies out of EagerImportTag --- .../jinjava/lib/tag/eager/EagerImportTag.java | 424 +----------------- .../lib/tag/eager/EagerIncludeTag.java | 3 +- .../AliasedEagerImportingStrategy.java | 279 ++++++++++++ .../importing/EagerImportingStrategy.java | 46 ++ .../EagerImportingStrategyFactory.java | 46 ++ .../importing/FlatEagerImportingStrategy.java | 103 +++++ .../tag/eager/importing/ImportingData.java | 40 ++ .../lib/tag/eager/EagerImportTagTest.java | 172 +++---- 8 files changed, 605 insertions(+), 508 deletions(-) create mode 100644 src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java create mode 100644 src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java index 69f928e67..1683b0ce5 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTag.java @@ -1,36 +1,21 @@ package com.hubspot.jinjava.lib.tag.eager; import com.google.common.annotations.Beta; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.hubspot.jinjava.interpret.Context; -import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.InterpretException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; -import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.lib.tag.DoTag; import com.hubspot.jinjava.lib.tag.ImportTag; -import com.hubspot.jinjava.loader.RelativePathResolver; -import com.hubspot.jinjava.objects.collections.PyMap; -import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; +import com.hubspot.jinjava.lib.tag.eager.importing.ImportingData; import com.hubspot.jinjava.tree.Node; import com.hubspot.jinjava.tree.parse.TagToken; import com.hubspot.jinjava.util.EagerReconstructionUtils; -import com.hubspot.jinjava.util.PrefixToPreserveState; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; -import java.util.StringJoiner; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; @Beta public class EagerImportTag extends EagerStateChangingTag { @@ -45,34 +30,22 @@ public EagerImportTag(ImportTag importTag) { @Override public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter) { - List helper = ImportTag.getHelpers(tagToken); - - String currentImportAlias = ImportTag.getContextVar(helper); + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, + interpreter + ); + EagerImportingStrategy eagerImportingStrategy = EagerImportingStrategyFactory.create( + importingData + ); - final String initialPathSetter = getSetTagForCurrentPath(interpreter); final String newPathSetter; Optional maybeTemplateFile; try { - maybeTemplateFile = ImportTag.getTemplateFile(helper, tagToken, interpreter); + maybeTemplateFile = + ImportTag.getTemplateFile(importingData.getHelpers(), tagToken, interpreter); } catch (DeferredValueException e) { - if (currentImportAlias.isEmpty()) { - throw e; - } - return ( - initialPathSetter + - new PrefixToPreserveState( - EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( - interpreter, - DeferredToken - .builderFromToken(tagToken) - .addUsedDeferredWords(Stream.of(helper.get(0))) - .addSetDeferredWords(Stream.of(currentImportAlias)) - .build() - ) - ) + - tagToken.getImage() - ); + return eagerImportingStrategy.handleDeferredTemplateFile(e); } if (!maybeTemplateFile.isPresent()) { return ""; @@ -80,7 +53,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter String templateFile = maybeTemplateFile.get(); try { Node node = ImportTag.parseTemplateAsNode(interpreter, templateFile); - newPathSetter = getSetTagForCurrentPath(interpreter); + newPathSetter = EagerImportingStrategyFactory.getSetTagForCurrentPath(interpreter); JinjavaInterpreter child = interpreter .getConfig() @@ -90,7 +63,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter JinjavaInterpreter.pushCurrent(child); String output; try { - setupImportAlias(currentImportAlias, child, interpreter); + eagerImportingStrategy.setup(child); output = child.render(node); } finally { JinjavaInterpreter.popCurrent(); @@ -109,7 +82,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter ) { ImportTag.handleDeferredNodesDuringImport( node, - currentImportAlias, + ImportTag.getContextVar(importingData.getHelpers()), childBindings, child, interpreter @@ -120,34 +93,12 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter tagToken.getStartPosition() ); } - integrateChild(currentImportAlias, childBindings, child, interpreter); - String finalOutput; + eagerImportingStrategy.integrateChild(child); if (child.getContext().getDeferredTokens().isEmpty() || output == null) { return ""; - } else if (!Strings.isNullOrEmpty(currentImportAlias)) { - // Since some values got deferred, output a DoTag that will load the currentImportAlias on the context. - finalOutput = - getFinalOutputWithAlias( - interpreter, - currentImportAlias, - initialPathSetter, - newPathSetter, - output, - childBindings - ); - } else { - finalOutput = - getFinalOutputWithoutAlias( - interpreter, - currentImportAlias, - initialPathSetter, - newPathSetter, - output, - childBindings - ); } return EagerReconstructionUtils.wrapInTag( - finalOutput, + eagerImportingStrategy.getFinalOutput(newPathSetter, output, childBindings), DoTag.TAG_NAME, interpreter, true @@ -164,343 +115,4 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter interpreter.getContext().getImportPathStack().pop(); } } - - private String getFinalOutputWithoutAlias( - JinjavaInterpreter interpreter, - String currentImportAlias, - String initialPathSetter, - String newPathSetter, - String output, - Map childBindings - ) { - return ( - newPathSetter + - getSetTagForDeferredChildBindings(interpreter, currentImportAlias, childBindings) + - output + - initialPathSetter - ); - } - - private String getFinalOutputWithAlias( - JinjavaInterpreter interpreter, - String currentImportAlias, - String initialPathSetter, - String newPathSetter, - String output, - Map childBindings - ) { - return ( - newPathSetter + - EagerReconstructionUtils.buildBlockOrInlineSetTagAndRegisterDeferredToken( - currentImportAlias, - Collections.emptyMap(), - interpreter - ) + - wrapInChildScope( - interpreter, - getSetTagForDeferredChildBindings( - interpreter, - currentImportAlias, - childBindings - ) + - output, - currentImportAlias - ) + - initialPathSetter - ); - } - - private static String wrapInChildScope( - JinjavaInterpreter interpreter, - String output, - String currentImportAlias - ) { - String combined = output + getDoTagToPreserve(interpreter, currentImportAlias); - // So that any set variables other than the alias won't exist outside the child's scope - return EagerReconstructionUtils.wrapInChildScope(combined, interpreter); - } - - private String getSetTagForDeferredChildBindings( - JinjavaInterpreter interpreter, - String currentImportAlias, - Map childBindings - ) { - if ( - Strings.isNullOrEmpty(currentImportAlias) && - interpreter.getContext().isDeferredExecutionMode() - ) { - Set metaContextVariables = interpreter - .getContext() - .getMetaContextVariables(); - // defer imported variables - EagerReconstructionUtils.buildSetTag( - childBindings - .entrySet() - .stream() - .filter( - entry -> - !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null - ) - .filter(entry -> !metaContextVariables.contains(entry.getKey())) - .collect(Collectors.toMap(Entry::getKey, entry -> "")), - interpreter, - true - ); - } - return childBindings - .entrySet() - .stream() - .filter( - entry -> - entry.getValue() instanceof DeferredValue && - ((DeferredValue) entry.getValue()).getOriginalValue() != null - ) - .filter(entry -> !interpreter.getContext().containsKey(entry.getKey())) - .filter(entry -> !entry.getKey().equals(currentImportAlias)) - .map( - entry -> - EagerReconstructionUtils.buildBlockOrInlineSetTag( // don't register deferred token so that we don't defer them on higher context scopes; they only exist in the child scope - entry.getKey(), - ((DeferredValue) entry.getValue()).getOriginalValue(), - interpreter - ) - ) - .collect(Collectors.joining()); - } - - public static String getSetTagForCurrentPath(JinjavaInterpreter interpreter) { - return EagerReconstructionUtils.buildBlockOrInlineSetTag( - RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, - interpreter - .getContext() - .getCurrentPathStack() - .peek() - .orElseGet( - () -> - (String) interpreter - .getContext() - .getOrDefault(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, "") - ), - interpreter - ); - } - - @SuppressWarnings("unchecked") - private static String getDoTagToPreserve( - JinjavaInterpreter interpreter, - String currentImportAlias - ) { - StringJoiner keyValueJoiner = new StringJoiner(","); - Object currentAliasMap = interpreter - .getContext() - .getSessionBindings() - .get(currentImportAlias); - for (Map.Entry entry : ( - (Map) ((DeferredValue) currentAliasMap).getOriginalValue() - ).entrySet()) { - if (entry.getKey().equals(currentImportAlias)) { - continue; - } - if (entry.getValue() instanceof DeferredValue) { - keyValueJoiner.add(String.format("'%s': %s", entry.getKey(), entry.getKey())); - } else if (!(entry.getValue() instanceof MacroFunction)) { - keyValueJoiner.add( - String.format( - "'%s': %s", - entry.getKey(), - PyishObjectMapper.getAsPyishString(entry.getValue()) - ) - ); - } - } - if (keyValueJoiner.length() > 0) { - return EagerReconstructionUtils.buildDoUpdateTag( - currentImportAlias, - "{" + keyValueJoiner.toString() + "}", - interpreter - ); - } - return ""; - } - - @VisibleForTesting - public static void setupImportAlias( - String currentImportAlias, - JinjavaInterpreter child, - JinjavaInterpreter parent - ) { - if (!Strings.isNullOrEmpty(currentImportAlias)) { - Optional maybeParentImportAlias = parent - .getContext() - .getImportResourceAlias(); - if (maybeParentImportAlias.isPresent()) { - child - .getContext() - .getScope() - .put( - Context.IMPORT_RESOURCE_ALIAS_KEY, - String.format("%s.%s", maybeParentImportAlias.get(), currentImportAlias) - ); - } else { - child - .getContext() - .getScope() - .put(Context.IMPORT_RESOURCE_ALIAS_KEY, currentImportAlias); - } - constructFullAliasPathMap(currentImportAlias, child); - getMapForCurrentContextAlias(currentImportAlias, child); - } - } - - @SuppressWarnings("unchecked") - private static void constructFullAliasPathMap( - String currentImportAlias, - JinjavaInterpreter child - ) { - String fullImportAlias = child - .getContext() - .getImportResourceAlias() - .orElse(currentImportAlias); - String[] allAliases = fullImportAlias.split("\\."); - Map currentMap = child.getContext().getParent(); - for (int i = 0; i < allAliases.length - 1; i++) { - Object maybeNextMap = currentMap.get(allAliases[i]); - if (maybeNextMap instanceof Map) { - currentMap = (Map) maybeNextMap; - } else if ( - maybeNextMap instanceof DeferredValue && - ((DeferredValue) maybeNextMap).getOriginalValue() instanceof Map - ) { - currentMap = - (Map) ((DeferredValue) maybeNextMap).getOriginalValue(); - } else { - throw new InterpretException("Encountered a problem with import alias maps"); - } - } - currentMap.put( - allAliases[allAliases.length - 1], - child.getContext().isDeferredExecutionMode() - ? DeferredValue.instance(new PyMap(new HashMap<>())) - : new PyMap(new HashMap<>()) - ); - } - - @SuppressWarnings("unchecked") - private static Map getMapForCurrentContextAlias( - String currentImportAlias, - JinjavaInterpreter child - ) { - Object parentValueForChild = child - .getContext() - .getParent() - .getSessionBindings() - .get(currentImportAlias); - if (parentValueForChild instanceof Map) { - return (Map) parentValueForChild; - } else if (parentValueForChild instanceof DeferredValue) { - if (((DeferredValue) parentValueForChild).getOriginalValue() instanceof Map) { - return (Map) ( - (DeferredValue) parentValueForChild - ).getOriginalValue(); - } - Map newMap = new PyMap(new HashMap<>()); - child - .getContext() - .getParent() - .put(currentImportAlias, DeferredValue.instance(newMap)); - return newMap; - } else { - Map newMap = new PyMap(new HashMap<>()); - child - .getContext() - .getParent() - .put( - currentImportAlias, - child.getContext().isDeferredExecutionMode() - ? DeferredValue.instance(newMap) - : newMap - ); - return newMap; - } - } - - @VisibleForTesting - public static void integrateChild( - String currentImportAlias, - Map childBindings, - JinjavaInterpreter child, - JinjavaInterpreter parent - ) { - for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { - if (parent.getContext().isDeferredExecutionMode()) { - macro.setDeferred(true); - } - } - if (StringUtils.isBlank(currentImportAlias)) { - for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { - parent.getContext().addGlobalMacro(macro); - } - childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); - childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); - Map childBindingsWithoutImportResourcePath = ImportTag.getChildBindingsWithoutImportResourcePath( - childBindings - ); - if (parent.getContext().isDeferredExecutionMode()) { - childBindingsWithoutImportResourcePath - .keySet() - .forEach( - key -> - parent - .getContext() - .put(key, DeferredValue.instance(parent.getContext().get(key))) - ); - } else { - parent.getContext().putAll(childBindingsWithoutImportResourcePath); - } - } else { - if ( - child.getContext().isDeferredExecutionMode() && - child - .getContext() - .getDeferredTokens() - .stream() - .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) - .collect(Collectors.toSet()) - .contains(currentImportAlias) - ) { - // since a child scope will be used, the import alias would not be properly reconstructed - throw new DeferredValueException( - "Same-named variable as import alias: " + currentImportAlias - ); - } - childBindings.putAll(child.getContext().getGlobalMacros()); - Map mapForCurrentContextAlias = getMapForCurrentContextAlias( - currentImportAlias, - child - ); - // Remove layers from self down to original import alias to prevent reference loops - Arrays - .stream( - child - .getContext() - .getImportResourceAlias() - .orElse(currentImportAlias) - .split("\\.") - ) - .filter( - key -> - mapForCurrentContextAlias == - ( - childBindings.get(key) instanceof DeferredValue - ? ((DeferredValue) childBindings.get(key)).getOriginalValue() - : childBindings.get(key) - ) - ) - .forEach(childBindings::remove); - // Remove meta keys - childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); - childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); - mapForCurrentContextAlias.putAll(childBindings); - } - } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java index 9fbc04f3f..b0054c47e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerIncludeTag.java @@ -3,6 +3,7 @@ import com.google.common.annotations.Beta; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.lib.tag.IncludeTag; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.tree.TagNode; import com.hubspot.jinjava.util.EagerReconstructionUtils; @@ -29,7 +30,7 @@ public String innerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) { tagNode.getStartPosition() ); templateFile = interpreter.resolveResourceLocation(templateFile); - final String initialPathSetter = EagerImportTag.getSetTagForCurrentPath( + final String initialPathSetter = EagerImportingStrategyFactory.getSetTagForCurrentPath( interpreter ); final String newPathSetter = EagerReconstructionUtils.buildBlockOrInlineSetTag( diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java new file mode 100644 index 000000000..588fc2d69 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/AliasedEagerImportingStrategy.java @@ -0,0 +1,279 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.annotations.VisibleForTesting; +import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.eager.DeferredToken; +import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.objects.serialization.PyishObjectMapper; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.PrefixToPreserveState; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AliasedEagerImportingStrategy implements EagerImportingStrategy { + private final ImportingData importingData; + private final String currentImportAlias; + + @VisibleForTesting + public AliasedEagerImportingStrategy( + ImportingData importingData, + String currentImportAlias + ) { + this.importingData = importingData; + this.currentImportAlias = currentImportAlias; + } + + @Override + public String handleDeferredTemplateFile(DeferredValueException e) { + return ( + importingData.getInitialPathSetter() + + new PrefixToPreserveState( + EagerReconstructionUtils.handleDeferredTokenAndReconstructReferences( + importingData.getOriginalInterpreter(), + DeferredToken + .builderFromToken(importingData.getTagToken()) + .addUsedDeferredWords(Stream.of(importingData.getHelpers().get(0))) + .addSetDeferredWords(Stream.of(currentImportAlias)) + .build() + ) + ) + + importingData.getTagToken().getImage() + ); + } + + @Override + public void setup(JinjavaInterpreter child) { + Optional maybeParentImportAlias = importingData + .getOriginalInterpreter() + .getContext() + .getImportResourceAlias(); + if (maybeParentImportAlias.isPresent()) { + child + .getContext() + .getScope() + .put( + Context.IMPORT_RESOURCE_ALIAS_KEY, + String.format("%s.%s", maybeParentImportAlias.get(), currentImportAlias) + ); + } else { + child + .getContext() + .getScope() + .put(Context.IMPORT_RESOURCE_ALIAS_KEY, currentImportAlias); + } + constructFullAliasPathMap(currentImportAlias, child); + getMapForCurrentContextAlias(currentImportAlias, child); + } + + @Override + public void integrateChild(JinjavaInterpreter child) { + JinjavaInterpreter parent = importingData.getOriginalInterpreter(); + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + if (parent.getContext().isDeferredExecutionMode()) { + macro.setDeferred(true); + } + } + if ( + child.getContext().isDeferredExecutionMode() && + child + .getContext() + .getDeferredTokens() + .stream() + .flatMap(deferredToken -> deferredToken.getSetDeferredWords().stream()) + .collect(Collectors.toSet()) + .contains(currentImportAlias) + ) { + // since a child scope will be used, the import alias would not be properly reconstructed + throw new DeferredValueException( + "Same-named variable as import alias: " + currentImportAlias + ); + } + Map childBindings = child.getContext().getSessionBindings(); + childBindings.putAll(child.getContext().getGlobalMacros()); + Map mapForCurrentContextAlias = getMapForCurrentContextAlias( + currentImportAlias, + child + ); + // Remove layers from self down to original import alias to prevent reference loops + Arrays + .stream( + child + .getContext() + .getImportResourceAlias() + .orElse(currentImportAlias) + .split("\\.") + ) + .filter( + key -> + mapForCurrentContextAlias == + ( + childBindings.get(key) instanceof DeferredValue + ? ((DeferredValue) childBindings.get(key)).getOriginalValue() + : childBindings.get(key) + ) + ) + .forEach(childBindings::remove); + // Remove meta keys + childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); + childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); + mapForCurrentContextAlias.putAll(childBindings); + } + + @Override + public String getFinalOutput( + String newPathSetter, + String output, + Map childBindings + ) { + return ( + newPathSetter + + EagerReconstructionUtils.buildBlockOrInlineSetTagAndRegisterDeferredToken( + currentImportAlias, + Collections.emptyMap(), + importingData.getOriginalInterpreter() + ) + + wrapInChildScope( + importingData.getOriginalInterpreter(), + EagerImportingStrategy.getSetTagForDeferredChildBindings( + importingData.getOriginalInterpreter(), + currentImportAlias, + childBindings + ) + + output, + currentImportAlias + ) + + importingData.getInitialPathSetter() + ); + } + + @SuppressWarnings("unchecked") + private static void constructFullAliasPathMap( + String currentImportAlias, + JinjavaInterpreter child + ) { + String fullImportAlias = child + .getContext() + .getImportResourceAlias() + .orElse(currentImportAlias); + String[] allAliases = fullImportAlias.split("\\."); + Map currentMap = child.getContext().getParent(); + for (int i = 0; i < allAliases.length - 1; i++) { + Object maybeNextMap = currentMap.get(allAliases[i]); + if (maybeNextMap instanceof Map) { + currentMap = (Map) maybeNextMap; + } else if ( + maybeNextMap instanceof DeferredValue && + ((DeferredValue) maybeNextMap).getOriginalValue() instanceof Map + ) { + currentMap = + (Map) ((DeferredValue) maybeNextMap).getOriginalValue(); + } else { + throw new InterpretException("Encountered a problem with import alias maps"); + } + } + currentMap.put( + allAliases[allAliases.length - 1], + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(new PyMap(new HashMap<>())) + : new PyMap(new HashMap<>()) + ); + } + + @SuppressWarnings("unchecked") + private static Map getMapForCurrentContextAlias( + String currentImportAlias, + JinjavaInterpreter child + ) { + Object parentValueForChild = child + .getContext() + .getParent() + .getSessionBindings() + .get(currentImportAlias); + if (parentValueForChild instanceof Map) { + return (Map) parentValueForChild; + } else if (parentValueForChild instanceof DeferredValue) { + if (((DeferredValue) parentValueForChild).getOriginalValue() instanceof Map) { + return (Map) ( + (DeferredValue) parentValueForChild + ).getOriginalValue(); + } + Map newMap = new PyMap(new HashMap<>()); + child + .getContext() + .getParent() + .put(currentImportAlias, DeferredValue.instance(newMap)); + return newMap; + } else { + Map newMap = new PyMap(new HashMap<>()); + child + .getContext() + .getParent() + .put( + currentImportAlias, + child.getContext().isDeferredExecutionMode() + ? DeferredValue.instance(newMap) + : newMap + ); + return newMap; + } + } + + private static String wrapInChildScope( + JinjavaInterpreter interpreter, + String output, + String currentImportAlias + ) { + String combined = output + getDoTagToPreserve(interpreter, currentImportAlias); + // So that any set variables other than the alias won't exist outside the child's scope + return EagerReconstructionUtils.wrapInChildScope(combined, interpreter); + } + + @SuppressWarnings("unchecked") + private static String getDoTagToPreserve( + JinjavaInterpreter interpreter, + String currentImportAlias + ) { + StringJoiner keyValueJoiner = new StringJoiner(","); + Object currentAliasMap = interpreter + .getContext() + .getSessionBindings() + .get(currentImportAlias); + for (Map.Entry entry : ( + (Map) ((DeferredValue) currentAliasMap).getOriginalValue() + ).entrySet()) { + if (entry.getKey().equals(currentImportAlias)) { + continue; + } + if (entry.getValue() instanceof DeferredValue) { + keyValueJoiner.add(String.format("'%s': %s", entry.getKey(), entry.getKey())); + } else if (!(entry.getValue() instanceof MacroFunction)) { + keyValueJoiner.add( + String.format( + "'%s': %s", + entry.getKey(), + PyishObjectMapper.getAsPyishString(entry.getValue()) + ) + ); + } + } + if (keyValueJoiner.length() > 0) { + return EagerReconstructionUtils.buildDoUpdateTag( + currentImportAlias, + "{" + keyValueJoiner.toString() + "}", + interpreter + ); + } + return ""; + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java new file mode 100644 index 000000000..cede53363 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategy.java @@ -0,0 +1,46 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.Map; +import java.util.stream.Collectors; + +public interface EagerImportingStrategy { + String handleDeferredTemplateFile(DeferredValueException e); + void setup(JinjavaInterpreter child); + + void integrateChild(JinjavaInterpreter child); + String getFinalOutput( + String newPathSetter, + String output, + Map childBindings + ); + + static String getSetTagForDeferredChildBindings( + JinjavaInterpreter interpreter, + String currentImportAlias, + Map childBindings + ) { + return childBindings + .entrySet() + .stream() + .filter( + entry -> + entry.getValue() instanceof DeferredValue && + ((DeferredValue) entry.getValue()).getOriginalValue() != null + ) + .filter(entry -> !interpreter.getContext().containsKey(entry.getKey())) + .filter(entry -> !entry.getKey().equals(currentImportAlias)) + .map( + entry -> + EagerReconstructionUtils.buildBlockOrInlineSetTag( // don't register deferred token so that we don't defer them on higher context scopes; they only exist in the child scope + entry.getKey(), + ((DeferredValue) entry.getValue()).getOriginalValue(), + interpreter + ) + ) + .collect(Collectors.joining()); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java new file mode 100644 index 000000000..cb3a227ba --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/EagerImportingStrategyFactory.java @@ -0,0 +1,46 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.base.Strings; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.tag.ImportTag; +import com.hubspot.jinjava.loader.RelativePathResolver; +import com.hubspot.jinjava.tree.parse.TagToken; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.List; + +public class EagerImportingStrategyFactory { + + public static ImportingData getImportingData( + TagToken tagToken, + JinjavaInterpreter interpreter + ) { + List helpers = ImportTag.getHelpers(tagToken); + String initialPathSetter = getSetTagForCurrentPath(interpreter); + return new ImportingData(interpreter, tagToken, helpers, initialPathSetter); + } + + public static EagerImportingStrategy create(ImportingData importingData) { + String currentImportAlias = ImportTag.getContextVar(importingData.getHelpers()); + if (Strings.isNullOrEmpty(currentImportAlias)) { + return new FlatEagerImportingStrategy(importingData); + } + return new AliasedEagerImportingStrategy(importingData, currentImportAlias); + } + + public static String getSetTagForCurrentPath(JinjavaInterpreter interpreter) { + return EagerReconstructionUtils.buildBlockOrInlineSetTag( + RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, + interpreter + .getContext() + .getCurrentPathStack() + .peek() + .orElseGet( + () -> + (String) interpreter + .getContext() + .getOrDefault(RelativePathResolver.CURRENT_PATH_CONTEXT_KEY, "") + ), + interpreter + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java new file mode 100644 index 000000000..bd7fbfea4 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/FlatEagerImportingStrategy.java @@ -0,0 +1,103 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.google.common.annotations.VisibleForTesting; +import com.hubspot.jinjava.interpret.Context; +import com.hubspot.jinjava.interpret.DeferredValue; +import com.hubspot.jinjava.interpret.DeferredValueException; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.lib.fn.MacroFunction; +import com.hubspot.jinjava.lib.tag.ImportTag; +import com.hubspot.jinjava.util.EagerReconstructionUtils; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +public class FlatEagerImportingStrategy implements EagerImportingStrategy { + private final ImportingData importingData; + + @VisibleForTesting + public FlatEagerImportingStrategy(ImportingData importingData) { + this.importingData = importingData; + } + + @Override + public String handleDeferredTemplateFile(DeferredValueException e) { + throw e; + } + + @Override + public void setup(JinjavaInterpreter child) { + // Do nothing + } + + @Override + public void integrateChild(JinjavaInterpreter child) { + JinjavaInterpreter parent = importingData.getOriginalInterpreter(); + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + if (parent.getContext().isDeferredExecutionMode()) { + macro.setDeferred(true); + } + } + for (MacroFunction macro : child.getContext().getGlobalMacros().values()) { + parent.getContext().addGlobalMacro(macro); + } + Map childBindings = child.getContext().getSessionBindings(); + + childBindings.remove(Context.GLOBAL_MACROS_SCOPE_KEY); + childBindings.remove(Context.IMPORT_RESOURCE_ALIAS_KEY); + Map childBindingsWithoutImportResourcePath = ImportTag.getChildBindingsWithoutImportResourcePath( + childBindings + ); + if (parent.getContext().isDeferredExecutionMode()) { + childBindingsWithoutImportResourcePath + .keySet() + .forEach( + key -> + parent + .getContext() + .put(key, DeferredValue.instance(parent.getContext().get(key))) + ); + } else { + parent.getContext().putAll(childBindingsWithoutImportResourcePath); + } + } + + @Override + public String getFinalOutput( + String newPathSetter, + String output, + Map childBindings + ) { + if (importingData.getOriginalInterpreter().getContext().isDeferredExecutionMode()) { + Set metaContextVariables = importingData + .getOriginalInterpreter() + .getContext() + .getMetaContextVariables(); + // defer imported variables + EagerReconstructionUtils.buildSetTag( + childBindings + .entrySet() + .stream() + .filter( + entry -> + !(entry.getValue() instanceof DeferredValue) && entry.getValue() != null + ) + .filter(entry -> !metaContextVariables.contains(entry.getKey())) + .collect(Collectors.toMap(Entry::getKey, entry -> "")), + importingData.getOriginalInterpreter(), + true + ); + } + return ( + newPathSetter + + EagerImportingStrategy.getSetTagForDeferredChildBindings( + importingData.getOriginalInterpreter(), + null, + childBindings + ) + + output + + importingData.getInitialPathSetter() + ); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java new file mode 100644 index 000000000..95b9de1b6 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/importing/ImportingData.java @@ -0,0 +1,40 @@ +package com.hubspot.jinjava.lib.tag.eager.importing; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.tree.parse.TagToken; +import java.util.List; + +public class ImportingData { + private final JinjavaInterpreter originalInterpreter; + private final TagToken tagToken; + private final List helpers; + private final String initialPathSetter; + + public ImportingData( + JinjavaInterpreter originalInterpreter, + TagToken tagToken, + List helpers, + String initialPathSetter + ) { + this.originalInterpreter = originalInterpreter; + this.tagToken = tagToken; + this.helpers = helpers; + this.initialPathSetter = initialPathSetter; + } + + public JinjavaInterpreter getOriginalInterpreter() { + return originalInterpreter; + } + + public TagToken getTagToken() { + return tagToken; + } + + public List getHelpers() { + return helpers; + } + + public String getInitialPathSetter() { + return initialPathSetter; + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java index 98436d7bf..ad7b65e3a 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerImportTagTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.common.base.Strings; import com.google.common.io.Resources; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.LegacyOverrides; @@ -12,11 +13,18 @@ import com.hubspot.jinjava.lib.tag.ImportTag; import com.hubspot.jinjava.lib.tag.ImportTagTest; import com.hubspot.jinjava.lib.tag.Tag; +import com.hubspot.jinjava.lib.tag.eager.importing.AliasedEagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory; +import com.hubspot.jinjava.lib.tag.eager.importing.FlatEagerImportingStrategy; +import com.hubspot.jinjava.lib.tag.eager.importing.ImportingData; import com.hubspot.jinjava.loader.LocationResolver; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.loader.ResourceLocator; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.collections.PyMap; +import com.hubspot.jinjava.tree.parse.DefaultTokenScannerSymbols; +import com.hubspot.jinjava.tree.parse.TagToken; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -33,6 +41,8 @@ public class EagerImportTagTest extends ImportTagTest { private static final String CONTEXT_VAR = "context_var"; private static final String TEMPLATE_FILE = "template.jinja"; + private TagToken tagToken; + @Before public void eagerSetup() throws Exception { context.put("padding", 42); @@ -57,6 +67,36 @@ public void eagerSetup() throws Exception { context.registerTag(tag); context.put("deferred", DeferredValue.instance()); JinjavaInterpreter.pushCurrent(interpreter); + tagToken = + new TagToken( + String.format("{%% import foo as %s %%}", CONTEXT_VAR), + 0, + 0, + new DefaultTokenScannerSymbols() + ); + } + + private AliasedEagerImportingStrategy getAliasedStrategy( + String alias, + JinjavaInterpreter parentInterpreter + ) { + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, + parentInterpreter + ); + + return new AliasedEagerImportingStrategy(importingData, alias); + } + + private FlatEagerImportingStrategy getFlatStrategy( + JinjavaInterpreter parentInterpreter + ) { + ImportingData importingData = EagerImportingStrategyFactory.getImportingData( + tagToken, + parentInterpreter + ); + + return new FlatEagerImportingStrategy(importingData); } @After @@ -70,7 +110,7 @@ public void itRemovesKeysFromChildBindings() { Map childBindings = child.getContext().getSessionBindings(); assertThat(childBindings.get(Context.IMPORT_RESOURCE_ALIAS_KEY)) .isEqualTo(CONTEXT_VAR); - EagerImportTag.integrateChild(CONTEXT_VAR, childBindings, child, interpreter); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat(((Map) interpreter.getContext().get(CONTEXT_VAR)).keySet()) .doesNotContain(Context.IMPORT_RESOURCE_ALIAS_KEY); @@ -83,18 +123,8 @@ public void itHandlesMultiLayer() { JinjavaInterpreter child2 = getChildInterpreter(child, ""); child2.getContext().put("foo", "foo val"); child.getContext().put("bar", "bar val"); - EagerImportTag.integrateChild( - "", - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - "", - child.getContext().getSessionBindings(), - child, - interpreter - ); + getFlatStrategy(child).integrateChild(child2); + getFlatStrategy(interpreter).integrateChild(child); assertThat(interpreter.getContext().get("foo")).isEqualTo("foo val"); assertThat(interpreter.getContext().get("bar")).isEqualTo("bar val"); } @@ -109,18 +139,9 @@ public void itHandlesMultiLayerAliased() { child2.render("{% set foo = 'foo val' %}"); child.render("{% set bar = 'bar val' %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) @@ -152,18 +173,9 @@ public void itHandlesMultiLayerAliasedAndDeferred() { child.render("{% set bar = 'bar val' %}"); child2.render("{% set foo_d = deferred %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(PyMap.class); assertThat( ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) @@ -199,18 +211,9 @@ public void itHandlesMultiLayerAliasedAndNullDeferred() { child.render("{% set bar = 'bar val' %}"); child2.render("{% set foo_d = deferred %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(PyMap.class); assertThat( ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) @@ -243,18 +246,8 @@ public void itHandlesMultiLayerDeferred() { child2.getContext().put("foo", DeferredValue.instance("foo val")); child.getContext().put("bar", DeferredValue.instance("bar val")); - EagerImportTag.integrateChild( - "", - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - "", - child.getContext().getSessionBindings(), - child, - interpreter - ); + getFlatStrategy(child).integrateChild(child2); + getFlatStrategy(interpreter).integrateChild(child); assertThat(interpreter.getContext().get("foo")).isInstanceOf(DeferredValue.class); assertThat( (((DeferredValue) (interpreter.getContext().get("foo"))).getOriginalValue()) @@ -271,34 +264,19 @@ public void itHandlesMultiLayerDeferred() { @Test @SuppressWarnings("unchecked") public void itHandlesMultiLayerSomeAliased() { - String child2Alias = ""; String child3Alias = "triple_child"; JinjavaInterpreter child = getChildInterpreter(interpreter, CONTEXT_VAR); - JinjavaInterpreter child2 = getChildInterpreter(child, child2Alias); + JinjavaInterpreter child2 = getChildInterpreter(child, ""); JinjavaInterpreter child3 = getChildInterpreter(child2, child3Alias); child2.render("{% set foo = 'foo val' %}"); child.render("{% set bar = 'bar val' %}"); child3.render("{% set foobar = 'foobar val' %}"); - EagerImportTag.integrateChild( - child3Alias, - child3.getContext().getSessionBindings(), - child3, - child2 - ); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child3Alias, child2).integrateChild(child3); + getFlatStrategy(child).integrateChild(child2); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child3Alias) @@ -337,24 +315,10 @@ public void itHandlesMultiLayerAliasedAndParallel() { child.render("{% set bar = 'bar val' %}"); child2B.render("{% set foo_b = 'foo_b val' %}"); - EagerImportTag.integrateChild( - child2Alias, - child2.getContext().getSessionBindings(), - child2, - child - ); - EagerImportTag.integrateChild( - child2BAlias, - child2B.getContext().getSessionBindings(), - child2B, - child - ); - EagerImportTag.integrateChild( - CONTEXT_VAR, - child.getContext().getSessionBindings(), - child, - interpreter - ); + getAliasedStrategy(child2Alias, child).integrateChild(child2); + getAliasedStrategy(child2BAlias, child).integrateChild(child2B); + getAliasedStrategy(CONTEXT_VAR, interpreter).integrateChild(child); + assertThat(interpreter.getContext().get(CONTEXT_VAR)).isInstanceOf(Map.class); assertThat( ((Map) interpreter.getContext().get(CONTEXT_VAR)).get(child2Alias) @@ -758,7 +722,7 @@ public void itDoesNotDeferImportedVariablesWhenNotInDeferredExecutionMode() { ); } - private static JinjavaInterpreter getChildInterpreter( + private JinjavaInterpreter getChildInterpreter( JinjavaInterpreter interpreter, String alias ) { @@ -767,7 +731,13 @@ private static JinjavaInterpreter getChildInterpreter( .getInterpreterFactory() .newInstance(interpreter); child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, TEMPLATE_FILE); - EagerImportTag.setupImportAlias(alias, child, interpreter); + EagerImportingStrategy eagerImportingStrategy; + if (Strings.isNullOrEmpty(alias)) { + eagerImportingStrategy = getFlatStrategy(interpreter); + } else { + eagerImportingStrategy = getAliasedStrategy(alias, interpreter); + } + eagerImportingStrategy.setup(child); return child; }