Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.squarespace.template.plugins.platform.CommerceFormatters Maven / Gradle / Ivy
/**
* Copyright (c) 2015 SQUARESPACE, Inc.
*
* 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.squarespace.template.plugins.platform;
import static com.squarespace.template.GeneralUtils.executeTemplate;
import static com.squarespace.template.GeneralUtils.getOrDefault;
import static com.squarespace.template.GeneralUtils.isTruthy;
import static com.squarespace.template.GeneralUtils.loadResource;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.squarespace.cldrengine.api.Decimal;
import com.squarespace.template.Arguments;
import com.squarespace.template.BaseFormatter;
import com.squarespace.template.CodeException;
import com.squarespace.template.CodeExecuteException;
import com.squarespace.template.Compiler;
import com.squarespace.template.Constants;
import com.squarespace.template.Context;
import com.squarespace.template.Formatter;
import com.squarespace.template.FormatterRegistry;
import com.squarespace.template.GeneralUtils;
import com.squarespace.template.Instruction;
import com.squarespace.template.JsonUtils;
import com.squarespace.template.StringView;
import com.squarespace.template.SymbolTable;
import com.squarespace.template.Variable;
import com.squarespace.template.Variables;
import com.squarespace.template.plugins.PluginUtils;
import com.squarespace.template.plugins.platform.enums.ProductType;
/**
* Extracted from Commons library at commit ab4ba7a6f2b872a31cb6449ae9e96f5f5b30f471
*/
public class CommerceFormatters implements FormatterRegistry {
@Override
public void registerFormatters(SymbolTable table) {
table.add(new AddToCartButtonFormatter());
table.add(new BookkeeperMoneyFormatter());
table.add(new CartQuantityFormatter());
table.add(new CartSubtotalFormatter());
table.add(new CartUrlFormatter());
table.add(new FromPriceFormatter());
table.add(new MoneyCamelFormatter());
table.add(new MoneyDashFormatter());
table.add(new MoneyStringFormatter());
table.add(new NormalPriceFormatter());
table.add(new PercentageFormatter());
table.add(new ProductCheckoutFormatter());
table.add(new ProductPriceFormatter());
table.add(new ProductQuickViewFormatter());
table.add(new ProductStatusFormatter());
table.add(new QuantityInputFormatter());
table.add(new SalePriceFormatter());
table.add(new SummaryFormFieldFormatter());
table.add(new VariantDescriptorFormatter());
table.add(new VariantsSelectFormatter());
table.add(new ProductScarcityFormatter());
table.add(new ProductRestockNotificationFormatter());
table.add(new SubscriptionPriceFormatter());
}
protected static class AddToCartButtonFormatter extends BaseFormatter {
private Instruction template;
public AddToCartButtonFormatter() {
super("add-to-cart-btn", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "add-to-cart-btn.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
var.set(executeTemplate(ctx, template, var.node(), false));
}
}
protected static class CartQuantityFormatter extends BaseFormatter {
public CartQuantityFormatter() {
super("cart-quantity", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
int count = 0;
JsonNode entriesNode = var.node().path("entries");
for (int i = 0; i < entriesNode.size(); i++) {
count += entriesNode.get(i).get("quantity").intValue();
}
StringBuilder buf = new StringBuilder();
buf.append("").append(count).append(" ");
var.set(buf);
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class CartSubtotalFormatter extends BaseFormatter {
public CartSubtotalFormatter() {
super("cart-subtotal", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
Decimal subtotalCents = new Decimal(var.node().path("subtotalCents").asText());
StringBuilder buf = new StringBuilder();
buf.append("");
CommerceUtils.writeLegacyMoneyString(subtotalCents, buf);
buf.append(" ");
var.set(buf);
}
}
protected static class CartUrlFormatter extends BaseFormatter {
public CartUrlFormatter() {
super("cart-url", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
variables.first().set("/cart");
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class FromPriceFormatter extends BaseFormatter {
public FromPriceFormatter() {
super("from-price", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable item = variables.first();
JsonNode moneyNode = CommerceUtils.getLowestPriceAmongVariants(item.node());
Decimal legacyPrice = CommerceUtils.getLegacyPriceFromMoneyNode(moneyNode);
item.set(legacyPrice.toString());
}
};
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected abstract static class MoneyBaseFormatter extends BaseFormatter {
public MoneyBaseFormatter(String identifier) {
super(identifier, false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
Decimal value = new Decimal(var.node().asText());
var.set(PluginUtils.formatMoney(value, Locale.US));
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class MoneyCamelFormatter extends MoneyBaseFormatter {
public MoneyCamelFormatter() {
super("moneyFormat");
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class MoneyDashFormatter extends MoneyBaseFormatter {
public MoneyDashFormatter() {
super("money-format");
}
}
protected static class BookkeeperMoneyFormatter extends BaseFormatter {
public BookkeeperMoneyFormatter() {
super("bookkeeper-money-format", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
double value = var.node().asDouble();
var.set(PlatformUtils.formatBookkeeperMoney(value, Locale.US));
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class MoneyStringFormatter extends BaseFormatter {
public MoneyStringFormatter() {
super("money-string", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
Decimal value = new Decimal(var.node().asText());
StringBuilder buf = new StringBuilder();
CommerceUtils.writeLegacyMoneyString(value, buf);
var.set(buf);
}
}
protected static class PercentageFormatter extends BaseFormatter {
public PercentageFormatter() {
super("percentage-format", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
double value = var.node().asDouble();
StringBuilder buf = new StringBuilder();
boolean trim = args.count() > 0 && args.first().equals("trim");
String formatted = PlatformUtils.formatPercentage(value, trim, Locale.US);
buf.append(formatted);
var.set(buf);
}
}
/**
* @deprecated this formatter is not used internally anymore. Remove it when all external usage is cleared.
*/
@Deprecated
protected static class NormalPriceFormatter extends BaseFormatter {
public NormalPriceFormatter() {
super("normal-price", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable item = variables.first();
JsonNode moneyNode = CommerceUtils.getHighestPriceAmongVariants(item.node());
Decimal legacyPrice = CommerceUtils.getLegacyPriceFromMoneyNode(moneyNode);
item.set(legacyPrice.toString());
}
}
protected static class ProductCheckoutFormatter extends BaseFormatter {
private static final String SOURCE = "{@|variants-select}{@|quantity-input}{@|add-to-cart-btn}";
private Instruction template;
public ProductCheckoutFormatter() {
super("product-checkout", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
this.template = compiler.compile(SOURCE).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
var.set(executeTemplate(ctx, template, var.node(), false));
}
}
protected static class ProductPriceFormatter extends BaseFormatter {
private Instruction template;
private static final String BILLING_PERIOD_MONTHLY = "MONTH";
private static final String BILLING_PERIOD_WEEKLY = "WEEK";
private static final String BILLING_PERIOD_YEARLY = "YEAR";
private static final Map PER_YEAR = new HashMap<>();
static {
PER_YEAR.put(BILLING_PERIOD_WEEKLY, 52);
PER_YEAR.put(BILLING_PERIOD_MONTHLY, 12);
}
public ProductPriceFormatter() {
super("product-price", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "product-price.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
StringBuilder buf = new StringBuilder();
ObjectNode obj = JsonUtils.createObjectNode();
if (CommerceUtils.isSubscribable(node)) {
resolveTemplateVariablesForSubscriptionProduct(ctx, node, obj);
} else if (CommerceUtils.getProductType(node) != ProductType.UNDEFINED) {
resolveTemplateVariablesForOTPProduct(ctx, node, obj);
}
JsonNode priceInfo = executeTemplate(ctx, template, obj, true);
buf.append(priceInfo.asText());
var.set(buf);
}
private static void resolveTemplateVariablesForOTPProduct(Context ctx, JsonNode productNode, ObjectNode args) {
if (CommerceUtils.hasVariedPrices(productNode)) {
args.put("fromText", StringUtils.defaultIfEmpty(
ctx.resolve(Constants.PRODUCT_PRICE_FROM_TEXT_KEY).asText(), "from {fromPrice}"));
args.put("formattedFromPrice", CommerceUtils.getMoneyString(CommerceUtils.getLowestPriceAmongVariants(productNode), ctx));
}
if (CommerceUtils.isOnSale(productNode)) {
args.put("formattedSalePriceText", "{price}");
args.put("formattedSalePrice", CommerceUtils.getMoneyString(CommerceUtils.getSalePriceMoneyNode(productNode), ctx));
}
args.put("formattedNormalPriceText", "{price}");
args.put("formattedNormalPrice", CommerceUtils.getMoneyString(CommerceUtils.getHighestPriceAmongVariants(productNode), ctx));
}
private static void resolveTemplateVariablesForSubscriptionProduct(
Context ctx, JsonNode productNode, ObjectNode args) {
JsonNode billingPeriodNode = CommerceUtils.getSubscriptionPlanBillingPeriodNode(productNode);
if (billingPeriodNode.isMissingNode()) {
args.put("formattedFromPrice", true);
args.put("fromText", StringUtils.defaultIfEmpty(
ctx.resolve(new String[] {"localizedStrings", "productPriceUnavailable"}).asText(), "Unavailable"));
return;
}
boolean hasMultiplePrices = CommerceUtils.hasVariedPrices(productNode);
int billingPeriodValue = CommerceUtils.getValueFromSubscriptionPlanBillingPeriod(billingPeriodNode);
String billingPeriodUnit = CommerceUtils.getUnitFromSubscriptionPlanBillingPeriod(billingPeriodNode);
int durationValue = billingPeriodValue * CommerceUtils.getNumBillingCyclesFromSubscriptionPlanNode(productNode);
String durationUnit = billingPeriodUnit;
// If the duration is a multiple of 52 weeks or 12 months, convert to years.
// Otherwise, use the billing period unit for the duration unit.
if (durationValue > 0 && PER_YEAR.containsKey(durationUnit) && durationValue % PER_YEAR.get(durationUnit) == 0) {
durationValue /= PER_YEAR.get(durationUnit);
durationUnit = BILLING_PERIOD_YEARLY;
}
args.put("billingPeriodValue", billingPeriodValue);
args.put("duration", durationValue);
// This string needs to match the correct translation template in v6 products-2.0-en-US.json.
StringBuilder i18nKeyBuilder = new StringBuilder("productPrice")
.append("__")
.append(hasMultiplePrices ? "multiplePrices" : "singlePrice")
.append("__")
.append(billingPeriodValue == 1 ? "1" : "n")
.append(StringUtils.capitalize(billingPeriodUnit.toLowerCase()) + "ly").append("__");
if (durationValue == 0) {
i18nKeyBuilder.append("indefinite");
} else {
i18nKeyBuilder.append("limited__")
.append(durationValue == 1 ? "1" : "n")
.append(StringUtils.capitalize(durationUnit.toLowerCase()) + "s");
}
String templateForPrice = StringUtils.defaultIfEmpty(
ctx.resolve(new String[] {"localizedStrings",
i18nKeyBuilder.toString()}).asText(), defaultSubscriptionPriceString(productNode));
if (hasMultiplePrices) {
args.put("fromText", templateForPrice);
args.put("formattedFromPrice", CommerceUtils.getMoneyString(CommerceUtils.getLowestPriceAmongVariants(productNode), ctx));
}
if (CommerceUtils.isOnSale(productNode)) {
args.put("formattedSalePriceText", templateForPrice);
args.put("formattedSalePrice", CommerceUtils.getMoneyString(CommerceUtils.getSalePriceMoneyNode(productNode), ctx));
}
args.put("formattedNormalPriceText", templateForPrice);
args.put("formattedNormalPrice", CommerceUtils.getMoneyString(CommerceUtils.getHighestPriceAmongVariants(productNode), ctx));
}
// TODO: This is shitty. The formatter should, if necessary, look up the English string and use it.
private static String defaultSubscriptionPriceString(JsonNode productNode) {
JsonNode billingPeriodNode = CommerceUtils.getSubscriptionPlanBillingPeriodNode(productNode);
boolean hasMultiplePrices = CommerceUtils.hasVariedPrices(productNode);
int billingPeriodValue = CommerceUtils.getValueFromSubscriptionPlanBillingPeriod(billingPeriodNode);
boolean billingPeriodPlural = billingPeriodValue > 1;
String billingPeriodUnit = CommerceUtils.getUnitFromSubscriptionPlanBillingPeriod(billingPeriodNode);
int numBillingCycles = CommerceUtils.getNumBillingCyclesFromSubscriptionPlanNode(productNode);
int durationValue = billingPeriodValue * numBillingCycles;
String durationUnit = billingPeriodUnit;
// If the duration is a multiple of 52 weeks or 12 months, convert to years.
// Otherwise, use the billing period unit for the duration unit.
if (durationValue > 0 && PER_YEAR.containsKey(durationUnit) && durationValue % PER_YEAR.get(durationUnit) == 0) {
durationValue /= PER_YEAR.get(durationUnit);
durationUnit = BILLING_PERIOD_YEARLY;
}
StringBuilder sb = new StringBuilder()
.append(hasMultiplePrices ? "from " : "")
.append("{price} every ")
.append(billingPeriodPlural ? "{billingPeriodValue} " : "")
.append(billingPeriodUnit.toLowerCase())
.append(billingPeriodPlural ? "s" : "");
if (numBillingCycles > 0) {
sb.append(" for {duration} ")
.append(durationUnit.toLowerCase())
.append(durationValue == 1 ? "" : "s");
}
return sb.toString();
}
}
protected static class SubscriptionPriceFormatter extends BaseFormatter {
private Instruction template;
public SubscriptionPriceFormatter() {
super("subscription-price", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "subscription-price.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
ObjectNode subscriptionResults = JsonUtils.createObjectNode();
JsonNode pricingOptions = CommerceUtils.getPricingOptionsAmongLowestVariant(node);
if (pricingOptions != null && pricingOptions.size() > 0) {
if (CommerceUtils.hasVariedPrices(node)) {
// This will return either salePriceMoney or priceMoney depending on whether the onSale is true or false.
// That's because this block here is the from {price} so the from price needs to be the lowest possible price
// taking into if a variant is onSale.
JsonNode subscriptionFromPricingNode = CommerceUtils.getSubscriptionMoneyFromFirstPricingOptions(pricingOptions);
subscriptionResults.put("fromText", StringUtils.defaultIfEmpty(
ctx.resolve(Constants.PRODUCT_PRICE_FROM_TEXT_KEY).asText(), "from {fromPrice}"));
subscriptionResults.put("formattedFromPrice", CommerceUtils.getMoneyString(subscriptionFromPricingNode, ctx));
}
JsonNode firstPricingOption = pricingOptions.get(0);
if (isOnSale(firstPricingOption)) {
subscriptionResults.put("formattedSubscriptionSalePriceText", "{price}");
subscriptionResults.put("formattedSubscriptionSalePrice", getSalePriceMoney(firstPricingOption, ctx));
}
subscriptionResults.put("formattedNormalSubscriptionPriceText", "{price}");
subscriptionResults.put("formattedNormalSubscriptionPrice", getPriceMoney(firstPricingOption, ctx));
}
JsonNode subscriptionPriceInfo = executeTemplate(ctx, template, subscriptionResults, true);
var.set(subscriptionPriceInfo.asText());
}
private static boolean isOnSale(JsonNode pricingOption) {
return isTruthy(pricingOption.path("onSale"));
}
private static String getSalePriceMoney(JsonNode pricingOption, Context ctx) {
return CommerceUtils.getMoneyString(pricingOption.path("salePriceMoney"), ctx);
}
private static String getPriceMoney(JsonNode pricingOption, Context ctx) {
return CommerceUtils.getMoneyString(pricingOption.path("priceMoney"), ctx);
}
}
protected static class ProductQuickViewFormatter extends BaseFormatter {
public ProductQuickViewFormatter() {
super("product-quick-view", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
String id = node.path("id").asText();
String group = args.isEmpty() ? "" : args.first();
// check to see if the group is a key that lives in the context or higher up
JsonNode groupNode = node.path(group);
if (!groupNode.isMissingNode()) {
group = groupNode.asText();
} else {
groupNode = ctx.resolve(group);
if (!groupNode.isMissingNode()) {
group = groupNode.asText();
}
}
StringBuilder buf = new StringBuilder();
buf.append("");
String text = ctx.resolve(Constants.PRODUCT_QUICK_VIEW_TEXT_KEY).asText();
buf.append(StringUtils.defaultIfEmpty(text, "Quick View"));
buf.append(" ");
var.set(buf);
}
}
protected static class ProductStatusFormatter extends BaseFormatter {
public ProductStatusFormatter() {
super("product-status", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
String productId = node.path("id").asText();
JsonNode productCtx = ctx.resolve("productMerchandisingContext");
String customSoldOutMessage = null;
if (productId != null && productCtx != null) {
customSoldOutMessage = productCtx.path(productId).path("customSoldOutText").asText();
}
StringBuilder buf = new StringBuilder();
if (CommerceUtils.isSoldOut(node)) {
String defaultSoldOutText = ctx.resolve(Constants.PRODUCT_SOLD_OUT_TEXT_KEY).asText();
String defaultSoldOutMessage = StringUtils.defaultIfEmpty(defaultSoldOutText, "sold out");
String soldOutMessage = StringUtils.defaultIfEmpty(customSoldOutMessage, defaultSoldOutMessage);
buf.append("");
PluginUtils.escapeHtmlAttribute(soldOutMessage, buf);
buf.append("
");
var.set(buf);
} else if (CommerceUtils.isOnSale(node)) {
String text = ctx.resolve(Constants.PRODUCT_SALE_TEXT_KEY).asText();
buf.append("");
buf.append(StringUtils.defaultIfEmpty(text, "sale"));
buf.append("
");
var.set(buf);
} else {
var.setMissing();
}
}
}
protected static class QuantityInputFormatter extends BaseFormatter {
private Instruction template;
public QuantityInputFormatter() {
super("quantity-input", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "quantity-input.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
ProductType type = CommerceUtils.getProductType(node);
boolean multipleQuantityAllowed = (ProductType.PHYSICAL.equals(type)
|| (ProductType.SERVICE.equals(type)
&& CommerceUtils.isMultipleQuantityAllowedForServices(ctx.resolve("websiteSettings"))))
&& !CommerceUtils.isSubscribable(node);
boolean hideQuantityInput = !multipleQuantityAllowed || CommerceUtils.getTotalStockRemaining(node) <= 1;
if (hideQuantityInput) {
var.setMissing();
return;
}
var.set(executeTemplate(ctx, template, var.node(), false));
}
}
protected static class SalePriceFormatter extends BaseFormatter {
public SalePriceFormatter() {
super("sale-price", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode moneyNode = CommerceUtils.getSalePriceMoneyNode(var.node());
Decimal legacyPrice = CommerceUtils.getLegacyPriceFromMoneyNode(moneyNode);
var.set(legacyPrice.toString());
}
}
protected static class VariantDescriptorFormatter extends BaseFormatter {
public VariantDescriptorFormatter() {
super("variant-descriptor", false);
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
StringBuilder buf = new StringBuilder();
CommerceUtils.writeVariantFormat(var.node(), buf);
var.set(buf);
}
}
protected static class VariantsSelectFormatter extends BaseFormatter {
private Instruction template;
private static final TextNode NAME_NODE = new TextNode("{name}");
public VariantsSelectFormatter() {
super("variants-select", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "variants-select.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode node = var.node();
ArrayNode options = CommerceUtils.getItemVariantOptions(node);
if (options.size() == 0) {
// Don't bother executing the template of nothing would be emitted.
var.setMissing();
return;
}
ObjectNode obj = JsonUtils.createObjectNode();
obj.set("item", node);
obj.set("displayText", getDisplayText(ctx, node));
obj.set("options", options);
obj.set("selectText", getSelectText(ctx, node));
var.set(executeTemplate(ctx, template, obj, false));
}
private static TextNode getDisplayText(Context ctx, JsonNode node) {
ProductType productType = CommerceUtils.getProductType(node);
// Gift Cards have variants forcibly named "Value" by default (as opposed to a merchant-defined variant name) and
// this string must be translated before being displayed on the front-end.
if (productType == ProductType.GIFT_CARD) {
String localizedDisplayName = ctx.resolve(Constants.GIFT_CARD_VALUE_DISPLAY_TEXT).asText();
return new TextNode(StringUtils.defaultIfEmpty(localizedDisplayName, "Value"));
}
return NAME_NODE;
}
private static TextNode getSelectText(Context ctx, JsonNode node) {
ProductType productType = CommerceUtils.getProductType(node);
// Gift Cards have variants forcibly named "Value" by default (as opposed to a merchant-defined variant name) and
// thus must be translated differently than other products. See COM-4912 for more details
if (productType == ProductType.GIFT_CARD) {
String localizedSelectText = ctx.resolve(Constants.GIFT_CARD_VARIANT_SELECT_TEXT).asText();
return new TextNode(StringUtils.defaultIfEmpty(localizedSelectText, "Select Value"));
} else {
String localizedSelectText = ctx.resolve(Constants.PRODUCT_VARIANT_SELECT_TEXT).asText();
return new TextNode(StringUtils.defaultIfEmpty(localizedSelectText, "Select {variantName}"));
}
}
}
protected static class SummaryFormFieldFormatter extends BaseFormatter {
private static final String[] TEMPLATES = new String[] {
"address", "checkbox", "date", "likert", "name", "phone", "time"
};
private final Map templateMap = new HashMap<>();
public SummaryFormFieldFormatter() {
super("summary-form-field", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
for (String type : TEMPLATES) {
String source = loadResource(CommerceFormatters.class, "summary-form-field-" + type + ".html");
Instruction code = compiler.compile(source.trim()).code();
templateMap.put(type, code);
}
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode field = var.node();
String type = field.path("type").asText();
Instruction code = templateMap.get(type);
JsonNode value = null;
if (code == null) {
value = field.path("value");
} else {
JsonNode node = field;
if (type.equals("likert")) {
Map answerMap = buildAnswerMap(ctx.resolve("localizedStrings"));
node = convertLikert(field.path("values"), answerMap);
}
value = executeTemplate(ctx, code, node, true);
}
// Assemble the HTML form wrapper containing the rendered value.
StringBuilder buf = new StringBuilder();
buf.append("\n");
buf.append(" ");
buf.append(field.path("rawTitle").asText());
buf.append(": ");
if (GeneralUtils.isTruthy(value)) {
buf.append(value.asText());
} else {
String text = ctx.resolve(Constants.PRODUCT_SUMMARY_FORM_NO_ANSWER_TEXT_KEY).asText();
buf.append(StringUtils.defaultIfEmpty(text, "N/A"));
}
buf.append("\n
");
var.set(buf);
}
private static JsonNode convertLikert(JsonNode values, Map answerMap) {
ArrayNode result = JsonUtils.createArrayNode();
Iterator> likertFields = values.fields();
while (likertFields.hasNext()) {
Entry likertField = likertFields.next();
String answer = likertField.getValue().asText();
ObjectNode node = JsonUtils.createObjectNode();
node.put("question", likertField.getKey());
node.put("answer", getOrDefault(answerMap, answer, answerMap.get("0")));
result.add(node);
}
return result;
}
private static final String KEY_PREFIX = "productAnswerMap";
private static final String KEY_STRONGLY_DISAGREE = KEY_PREFIX + "StronglyDisagree";
private static final String KEY_DISAGREE = KEY_PREFIX + "Disagree";
private static final String KEY_NEUTRAL = KEY_PREFIX + "Neutral";
private static final String KEY_AGREE = KEY_PREFIX + "Agree";
private static final String KEY_STRONGLY_AGREE = KEY_PREFIX + "StronglyAgree";
private Map buildAnswerMap(JsonNode strings) {
Map map = new HashMap<>();
map.put("-2", GeneralUtils.localizeOrDefault(strings, KEY_STRONGLY_DISAGREE, "Strongly Disagree"));
map.put("-1", GeneralUtils.localizeOrDefault(strings, KEY_DISAGREE, "Disagree"));
map.put("0", GeneralUtils.localizeOrDefault(strings, KEY_NEUTRAL, "Neutral"));
map.put("1", GeneralUtils.localizeOrDefault(strings, KEY_AGREE, "Agree"));
map.put("2", GeneralUtils.localizeOrDefault(strings, KEY_STRONGLY_AGREE, "Strongly Agree"));
return map;
}
}
protected static class ProductScarcityFormatter extends BaseFormatter {
private Instruction template;
ProductScarcityFormatter() {
super("product-scarcity", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "product-scarcity.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
JsonNode productMerchandisingContext = ctx.resolve("productMerchandisingContext");
Variable var = variables.first();
JsonNode product = var.node();
if (productMerchandisingContext.isMissingNode()) {
return;
}
// Find the merchandising context for this product
String productId = product.get("id").asText();
JsonNode contextForProduct = productMerchandisingContext.path(productId);
if (contextForProduct.isMissingNode()) {
return;
}
if (contextForProduct.get("scarcityEnabled").asBoolean()) {
ObjectNode templateVariables = JsonUtils.createObjectNode();
templateVariables.put("scarcityTemplateViews", contextForProduct.get("scarcityTemplateViews"));
templateVariables.put("scarcityText", contextForProduct.get("scarcityText"));
templateVariables.put("scarcityShownByDefault", contextForProduct.get("scarcityShownByDefault"));
var.set(executeTemplate(ctx, template, templateVariables, false));
}
}
}
protected static class ProductRestockNotificationFormatter extends BaseFormatter {
private Instruction template;
ProductRestockNotificationFormatter() {
super("product-restock-notification", false);
}
@Override
public void initialize(Compiler compiler) throws CodeException {
String source = loadResource(CommerceFormatters.class, "product-restock-notification.html");
this.template = compiler.compile(source.trim()).code();
}
@Override
public void apply(Context ctx, Arguments args, Variables variables) throws CodeExecuteException {
Variable var = variables.first();
JsonNode product = var.node();
JsonNode websiteCtx = ctx.resolve("website");
JsonNode productCtx = ctx.resolve("productMerchandisingContext");
String productId = product.get("id").asText();
JsonNode productNode = productCtx.path(productId);
ObjectNode templateVariables = JsonUtils.createObjectNode();
templateVariables.set("product", product);
templateVariables.set("views", productNode.path("restockNotificationViews"));
templateVariables.set("messages", productNode.path("restockNotificationMessages"));
templateVariables.set("mailingListSignUpEnabled", productNode.path("mailingListSignUpEnabled"));
templateVariables.set("mailingListOptInByDefault", productNode.path("mailingListOptInByDefault"));
templateVariables.set("captchaSiteKey", websiteCtx.path("captchaSettings").path("siteKey"));
var.set(executeTemplate(ctx, template, templateVariables, true));
}
}
}