/* * Copyright (C) 2014 Servoy BV * * 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.servoy.j2db.server.ngclient.property.types; import java.util.HashSet; import java.util.Set; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import org.mozilla.javascript.Scriptable; import org.sablo.BaseWebObject; import org.sablo.specification.PropertyDescription; import org.sablo.specification.property.IBrowserConverterContext; import org.sablo.specification.property.IConvertedPropertyType; import org.sablo.specification.property.types.DefaultPropertyType; import org.sablo.util.ValueReference; import org.sablo.websocket.utils.DataConversion; import org.sablo.websocket.utils.JSONUtils; import com.servoy.base.util.ITagResolver; import com.servoy.j2db.FlattenedSolution; import com.servoy.j2db.persistence.ScriptVariable; import com.servoy.j2db.server.ngclient.DataAdapterList; import com.servoy.j2db.server.ngclient.FormElement; import com.servoy.j2db.server.ngclient.FormElementContext; import com.servoy.j2db.server.ngclient.HTMLTagsConverter; import com.servoy.j2db.server.ngclient.IContextProvider; import com.servoy.j2db.server.ngclient.INGApplication; import com.servoy.j2db.server.ngclient.INGFormElement; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.property.ICanBeLinkedToFoundset; import com.servoy.j2db.server.ngclient.property.types.NGConversions.IFormElementToSabloComponent; import com.servoy.j2db.server.ngclient.property.types.NGConversions.IFormElementToTemplateJSON; import com.servoy.j2db.server.ngclient.property.types.NGConversions.IRhinoToSabloComponent; import com.servoy.j2db.server.ngclient.property.types.NGConversions.ISabloComponentToRhino; import com.servoy.j2db.util.HtmlUtils; import com.servoy.j2db.util.ScopesUtils; import com.servoy.j2db.util.Text; /** * Property type that handles smart text properties (aware of i18n and dataprovider/special tag usage %%...%%). * * @author jcompagner * @author acostescu */ public class TagStringPropertyType extends DefaultPropertyType<BasicTagStringTypeSabloValue> implements IFormElementToTemplateJSON<String, BasicTagStringTypeSabloValue>, ISupportTemplateValue<String>, IDataLinkedType<String, BasicTagStringTypeSabloValue>, IFormElementToSabloComponent<String, BasicTagStringTypeSabloValue>, IConvertedPropertyType<BasicTagStringTypeSabloValue>, ISabloComponentToRhino<BasicTagStringTypeSabloValue>, IRhinoToSabloComponent<BasicTagStringTypeSabloValue>, ICanBeLinkedToFoundset<String, BasicTagStringTypeSabloValue> { public static final TagStringPropertyType INSTANCE = new TagStringPropertyType(); public static final String TYPE_NAME = "tagstring"; private TagStringPropertyType() { } @Override public String getName() { return TYPE_NAME; } @Override public TagStringConfig parseConfig(JSONObject json) { // see TagStringConfig docs for what the defaults mean String displayTagsPropertyName = null; boolean displayTags = true; boolean useParsedValueInRhino = false; // String forFoundsetPropertyName = null; // see FoundsetLinkedPropertyType for how tagstrings linked to foundsets work if (json != null) { // see TagStringConfig docs for what the defaults mean displayTagsPropertyName = json.optString(TagStringConfig.DISPLAY_TAGS_PROPERTY_NAME_CONFIG_OPT, null); displayTags = json.optBoolean(TagStringConfig.DISPLAY_TAGS_CONFIG_OPT, true); useParsedValueInRhino = json.optBoolean(TagStringConfig.USE_PARSED_VALUE_IN_RHINO_CONFIG_OPT, false); } return new TagStringConfig(displayTagsPropertyName, displayTags, useParsedValueInRhino); } protected TagStringConfig getConfig(PropertyDescription pd) { return (TagStringConfig)pd.getConfig(); } @Override public JSONWriter toTemplateJSONValue(JSONWriter writer, String key, String formElementValue, PropertyDescription pd, DataConversion browserConversionMarkers, FormElementContext formElementContext) throws JSONException { // TODO when type has more stuff added to it, see if this needs to be changed (what is put in form cached templates for such properties) JSONUtils.addKeyIfPresent(writer, key); if (formElementValue != null && valueInTemplate(formElementValue, pd, formElementContext)) { writer.value(formElementValue); } else { writer.value(""); } return writer; } @Override public BasicTagStringTypeSabloValue fromJSON(Object newValue, BasicTagStringTypeSabloValue previousValue, PropertyDescription pd, IBrowserConverterContext dataConverterContext, ValueReference<Boolean> returnValueAdjustedIncommingValue) { BaseWebObject webObject = dataConverterContext.getWebObject(); return createNewTagStringTypeSabloValue((String)newValue, (previousValue != null ? previousValue.getDataAdapterList() : null), false, false, pd, webObject instanceof WebFormComponent ? ((WebFormComponent)webObject) : null, ((IContextProvider)dataConverterContext.getWebObject()).getDataConverterContext().getApplication(), false); } protected BasicTagStringTypeSabloValue createNewTagStringTypeSabloValue(String designValue, DataAdapterList dataAdapterList, boolean tagParsingAllowed, boolean htmlParsingAllowed, PropertyDescription propertyDescription, WebFormComponent component, INGApplication application, boolean basedOnFormElementValue) { BasicTagStringTypeSabloValue sabloValue; TagStringConfig config = (TagStringConfig)propertyDescription.getConfig(); boolean wouldLikeToParseTags = wouldLikeToParseTags(config, component.getFormElement()); // this setting is decided at design/form-element time and won't change even if the value gets changed from rhino // if "wouldLikeToParseTags && !config.useParsedValueInRhino()" is true, we will never have a null previous value so we can still reach DAL // the "&& !config.useParsedValueInRhino()" is an optimization; because if config.useParsedValueInRhino() is true, then no new value set from Rhino or scripting will be able to handle tags any more - so there's no need to hang on to DAL (if this changes, you can remove this check) boolean needsToKeepDAL = (wouldLikeToParseTags && !config.useParsedValueInRhino()); DataAdapterList dal = (needsToKeepDAL ? dataAdapterList : null); String newDesignValue = designValue != null && designValue.startsWith("i18n:") ? application.getI18NMessage(designValue.toString().substring(5)) : designValue; if (newDesignValue == null) { sabloValue = needsToKeepDAL ? new BasicTagStringTypeSabloValue(null, dal) : null; } else if (tagParsingAllowed && wouldLikeToParseTags && newDesignValue.contains("%%")) // tagParsingAllowed is a security feature so that browsers cannot change tagStrings to something that is then able to show random server-side data { boolean i18nReplaced = !newDesignValue.equals(designValue); // TODO currently htmlParsingAllowed will be true here as well (the method is never called with true/false); but if that is needed in the future, we need to let TagStringTypeSabloValue of htmlParsingAllowed == false as well) // data links are required; register them to DAL; normally DAL can't be null here sabloValue = new TagStringTypeSabloValue(newDesignValue, dal, component.getDataConverterContext(), propertyDescription, component.getFormElement(), basedOnFormElementValue && !i18nReplaced); } else // just some static string { String staticValue = newDesignValue; if (htmlParsingAllowed && HtmlUtils.startsWithHtml(staticValue)) // htmlParsingAllowed is a security feature so that browsers cannot change tagStrings to something that is then able to execute random server-side javascript { staticValue = HTMLTagsConverter.convert(staticValue, component.getDataConverterContext(), false); } // no data links required sabloValue = new BasicTagStringTypeSabloValue(staticValue, dal); } return sabloValue; } @Override public JSONWriter toJSON(JSONWriter writer, String key, BasicTagStringTypeSabloValue object, PropertyDescription pd, DataConversion clientConversion, IBrowserConverterContext dataConverterContext) throws JSONException { if (object != null) { object.toJSON(writer, key, clientConversion, dataConverterContext); } else { JSONUtils.addKeyIfPresent(writer, key); writer.value(""); } return writer; } @Override public boolean valueInTemplate(String formElementVal, PropertyDescription pd, FormElementContext formElementContext) { if (formElementVal == null) return true; TagStringConfig config = ((TagStringConfig)pd.getConfig()); // TODO - it could still return "value" even for HTML if we know HTMLTagsConverter.convert() would not want to touch that (so simple HTML) // but we don't want to expose the actual design-time stuff that would normally get encrypted by HTMLTagsConverter.convert() or is not yet valid (blobloader without an application instance for example). return !((wouldLikeToParseTags(config, formElementContext.getFormElement()) && formElementVal.contains("%%")) || formElementVal.startsWith("i18n:") || HtmlUtils.startsWithHtml(formElementVal)); } /** * Checks the component's spec. configurations options and form element properties (if needed) * to see if this property should parse tags (%%x%%) or not. */ protected boolean wouldLikeToParseTags(TagStringConfig config, FormElement formElement) { String dtpn = config.getDisplayTagsPropertyName(); Object dtPropVal = null; if (dtpn != null) { dtPropVal = formElement.getPropertyValue(dtpn); if (dtPropVal == null) dtPropVal = Boolean.FALSE; } return (dtpn != null && ((Boolean)dtPropVal).booleanValue() == true) || (dtpn == null && config.shouldDisplayTags()); } @Override public TargetDataLinks getDataLinks(String formElementValue, PropertyDescription pd, FlattenedSolution flattenedSolution, final FormElement formElement) { final Set<String> dataProviders = new HashSet<>(); final boolean recordDP[] = new boolean[1]; Text.processTags(formElementValue, new ITagResolver() { @Override public String getStringValue(String name) { String dp = name; if (dp.startsWith(ScriptVariable.GLOBALS_DOT_PREFIX)) { dp = ScriptVariable.SCOPES_DOT_PREFIX + dp; } dataProviders.add(dp); // TODO Can't it be something special like record count or current record which are special cases and could still not depend on record...? recordDP[0] = recordDP[0] || (!ScopesUtils.isVariableScope(dp) && formElement.getForm().getScriptVariable(dp) == null); return dp; } }); return dataProviders.size() == 0 ? TargetDataLinks.NOT_LINKED_TO_DATA : new TargetDataLinks(dataProviders.toArray(new String[dataProviders.size()]), recordDP[0]); } @Override public BasicTagStringTypeSabloValue toSabloComponentValue(String formElementValue, PropertyDescription pd, INGFormElement formElement, WebFormComponent component, DataAdapterList dataAdapterList) { return createNewTagStringTypeSabloValue(formElementValue, dataAdapterList, true, true, pd, component, ((IContextProvider)component).getDataConverterContext().getApplication(), true); } @Override public BasicTagStringTypeSabloValue toSabloComponentValue(Object rhinoValue, BasicTagStringTypeSabloValue previousComponentValue, PropertyDescription pd, BaseWebObject componentOrService) { if (rhinoValue != null) { // this code can interpret the new value as a static one or a a tag-aware one depending on the property's config: USE_PARSED_VALUE_IN_RHINO_CONFIG_OPT String newDesignValue = rhinoValue instanceof String ? (String)rhinoValue : rhinoValue.toString(); return createNewTagStringTypeSabloValue(newDesignValue, (previousComponentValue != null ? previousComponentValue.getDataAdapterList() : null), !((TagStringConfig)pd.getConfig()).useParsedValueInRhino(), true, pd, componentOrService instanceof WebFormComponent ? ((WebFormComponent)componentOrService) : null, ((IContextProvider)componentOrService).getDataConverterContext().getApplication(), false); } return null; } @Override public boolean isValueAvailableInRhino(BasicTagStringTypeSabloValue webComponentValue, PropertyDescription pd, BaseWebObject componentOrService) { return true; } @Override public Object toRhinoValue(BasicTagStringTypeSabloValue webComponentValue, PropertyDescription pd, BaseWebObject componentOrService, Scriptable startScriptable) { if (webComponentValue == null) return null; if (((TagStringConfig)pd.getConfig()).useParsedValueInRhino()) return webComponentValue.getTagReplacedValue(); else return webComponentValue.getDesignValue(); } }