diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2f6274..69a4fa88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## Version 3.15.1 + +### Date: 24-June-2024 + +- added support to convert json to html + +--- + ## Version 3.15.0 ### Date: 20-May-2024 diff --git a/contentstack/build.gradle b/contentstack/build.gradle index 035e17c7..9e9f1fb9 100755 --- a/contentstack/build.gradle +++ b/contentstack/build.gradle @@ -10,7 +10,7 @@ android.buildFeatures.buildConfig true mavenPublishing { publishToMavenCentral(SonatypeHost.DEFAULT) signAllPublications() - coordinates("com.contentstack.sdk", "android", "3.15.0") + coordinates("com.contentstack.sdk", "android", "3.15.1") pom { name = "contentstack-android" @@ -76,6 +76,11 @@ android { // } } } + // signing { + // // Specify key and other signing details + // useGpgCmd() + // sign configurations.archives + // } signingConfigs { debug { storeFile file("../key.keystore") @@ -111,12 +116,12 @@ android { testCoverageEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - buildConfigField "String", "host", localProperties['host'] - buildConfigField "String", "APIKey", localProperties['APIKey'] - buildConfigField "String", "deliveryToken", localProperties['deliveryToken'] - buildConfigField "String", "environment", localProperties['environment'] - buildConfigField "String", "contentTypeUID", localProperties['contentType'] - buildConfigField "String", "assetUID", localProperties['assetUid'] + buildConfigField "String", "host", localProperties['host'] + buildConfigField "String", "APIKey", localProperties['APIKey'] + buildConfigField "String", "deliveryToken", localProperties['deliveryToken'] + buildConfigField "String", "environment", localProperties['environment'] + buildConfigField "String", "contentTypeUID", localProperties['contentType'] + buildConfigField "String", "assetUID", localProperties['assetUid'] } release { minifyEnabled false diff --git a/contentstack/src/main/java/com/contentstack/sdk/DefaultOption.java b/contentstack/src/main/java/com/contentstack/sdk/DefaultOption.java new file mode 100644 index 00000000..39eff7d8 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/DefaultOption.java @@ -0,0 +1,216 @@ +package com.contentstack.sdk; + +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +public class DefaultOption implements Option { + @Override + public String renderOptions(JSONObject embeddedObject, Metadata metadata) { + switch (metadata.getStyleType()) { + case BLOCK: + return "

" + findTitleOrUid(embeddedObject) + "

Content type: " + embeddedObject.optString("_content_type_uid") + "

"; + case INLINE: + return "" + findTitleOrUid(embeddedObject) + ""; + case LINK: + return "" + findTitleOrUid(embeddedObject) + ""; + case DISPLAY: + return "\"""; + default: + return ""; + } + } + + @Override + public String renderMark(MarkType markType, String text) { + switch (markType) { + case SUPERSCRIPT: + return "" + text + ""; + case SUBSCRIPT: + return "" + text + ""; + case INLINECODE: + return "" + text + ""; + case STRIKETHROUGH: + return "" + text + ""; + case UNDERLINE: + return "" + text + ""; + case ITALIC: + return "" + text + ""; + case BOLD: + return "" + text + ""; + case BREAK: + return "
" + text.replace("\n", ""); + default: + return text; + } + } + + private String escapeInjectHtml(JSONObject nodeObj, String nodeType) { + String injectedHtml = getNodeStr(nodeObj, nodeType); + return TextUtils.htmlEncode(injectedHtml); + } + + @Override + public String renderNode(String nodeType, JSONObject nodeObject, NodeCallback callback) { + String strAttrs = strAttrs(nodeObject); + String children = callback.renderChildren(nodeObject.optJSONArray("children")); + switch (nodeType) { + case "p": + return "" + children + "

"; + case "a": + return "" + children + ""; + case "img": + String assetLink = getNodeStr(nodeObject, "asset-link"); + if (!assetLink.isEmpty()) { + JSONObject attrs = nodeObject.optJSONObject("attrs"); + if (attrs.has("link")) { + return "" + "" + children + ""; + } + return "" + children; + } + return "" + children; + case "embed": + return ""; + case "h1": + return "" + children + ""; + case "h2": + return "" + children + ""; + case "h3": + return "" + children + ""; + case "h4": + return "" + children + ""; + case "h5": + return "" + children + ""; + case "h6": + return "" + children + ""; + case "ol": + return "" + children + ""; + case "ul": + return "" + children + ""; + case "li": + return "" + children + ""; + case "hr": + return ""; + case "table": + return "" + children + "
"; + case "thead": + return "" + children + ""; + case "tbody": + return "" + children + ""; + case "tfoot": + return "" + children + ""; + case "tr": + return "" + children + ""; + case "th": + return "" + children + ""; + case "td": + return "" + children + ""; + case "blockquote": + return "" + children + ""; + case "code": + return "" + children + ""; + case "reference": + return ""; + case "fragment": + return "" + children + ""; + default: + return children; + } + } + + String strAttrs(JSONObject nodeObject) { + StringBuilder result = new StringBuilder(); + if (nodeObject.has("attrs")) { + JSONObject attrsObject = nodeObject.optJSONObject("attrs"); + if (attrsObject != null && attrsObject.length() > 0) { + for (Iterator it = attrsObject.keys(); it.hasNext(); ) { + String key = it.next(); + Object objValue = attrsObject.opt(key); + String value = objValue.toString(); + // If style is available, do styling calculations + if (Objects.equals(key, "style")) { + String resultStyle = stringifyStyles(attrsObject.optJSONObject("style")); + result.append(" ").append(key).append("=\"").append(resultStyle).append("\""); + } else { + String[] ignoreKeys = {"href", "asset-link", "src", "url"}; + ArrayList ignoreKeysList = new ArrayList<>(Arrays.asList(ignoreKeys)); + if (!ignoreKeysList.contains(key)) { + result.append(" ").append(key).append("=\"").append(value).append("\""); + } + } + } + } + } + return result.toString(); + } + + private String stringifyStyles(JSONObject style) { + Map styleMap = new HashMap<>(); + + // Convert JSONObject to a Map + Iterator keys = style.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String value = null; + try { + value = style.getString(key); + } catch (JSONException e) { + throw new RuntimeException(e); + } + styleMap.put(key, value); + } + + StringBuilder styleString = new StringBuilder(); + + for (Map.Entry entry : styleMap.entrySet()) { + String property = entry.getKey(); + String value = entry.getValue(); + + styleString.append(property).append(": ").append(value).append("; "); + } + + return styleString.toString(); + } + + private String getNodeStr(JSONObject nodeObject, String key) { + String herf = nodeObject.optJSONObject("attrs").optString(key); // key might be [href/src] + if (herf == null || herf.isEmpty()) { + herf = nodeObject.optJSONObject("attrs").optString("url"); + } + return herf; + } + + protected String findTitleOrUid(JSONObject embeddedObject) { + String _title = ""; + if (embeddedObject != null) { + if (embeddedObject.has("title") && !embeddedObject.optString("title").isEmpty()) { + _title = embeddedObject.optString("title"); + } else if (embeddedObject.has("uid")) { + _title = embeddedObject.optString("uid"); + } + } + return _title; + } + + protected String findAssetTitle(JSONObject embeddedObject) { + String _title = ""; + if (embeddedObject != null) { + if (embeddedObject.has("title") && !embeddedObject.optString("title").isEmpty()) { + _title = embeddedObject.optString("title"); + } else if (embeddedObject.has("filename")) { + _title = embeddedObject.optString("filename"); + } else if (embeddedObject.has("uid")) { + _title = embeddedObject.optString("uid"); + } + } + return _title; + } +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Metadata.java b/contentstack/src/main/java/com/contentstack/sdk/Metadata.java new file mode 100644 index 00000000..aac18643 --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Metadata.java @@ -0,0 +1,104 @@ +package com.contentstack.sdk; + +import java.util.jar.Attributes; + +public class Metadata { + String text; + String itemType; + String itemUid; + String contentTypeUid; + StyleType styleType; + String outerHTML; + Attributes attributes; + + public Metadata(String text, String itemType, String itemUid, String contentTypeUid, + String styleType, String outerHTML, Attributes attributes) { + this.text = text; + this.itemType = itemType; + this.itemUid = itemUid; + this.contentTypeUid = contentTypeUid; + this.styleType = StyleType.valueOf(styleType.toUpperCase()); + this.outerHTML = outerHTML; + this.attributes = attributes; + } + + @Override + public String toString() { + return "EmbeddedObject{" + + "text='" + text + '\'' + + "type='" + itemType + '\'' + + ", uid='" + itemUid + '\'' + + ", contentTypeUid='" + contentTypeUid + '\'' + + ", sysStyleType=" + styleType + + ", outerHTML='" + outerHTML + '\'' + + ", attributes='" + attributes + '\'' + + '}'; + } + + /** + * The getText() function returns the value of the text variable. + * + * @return The method is returning a String value. + */ + public String getText() { + return text; + } + + /** + * The getItemType() function returns the type of an item. + * + * @return The method is returning the value of the variable "itemType". + */ + public String getItemType() { + return itemType; + } + + /** + * The function returns the attributes of an object. + * + * @return The method is returning an object of type Attributes. + */ + public Attributes getAttributes() { + return attributes; + } + + /** + * The getItemUid() function returns the itemUid value. + * + * @return The method is returning the value of the variable "itemUid". + */ + public String getItemUid() { + return itemUid; + } + + /** + * The function returns the content type UID as a string. + * + * @return The method is returning the value of the variable "contentTypeUid". + */ + public String getContentTypeUid() { + return contentTypeUid; + } + + /** + * The function returns the value of the styleType variable. + * + * @return The method is returning the value of the variable "styleType" of type StyleType. + */ + public StyleType getStyleType() { + return styleType; + } + + /** + * The getOuterHTML() function returns the outer HTML of an element. + * + * @return The method is returning the value of the variable "outerHTML". + */ + public String getOuterHTML() { + return outerHTML; + } +} + +enum StyleType { + BLOCK, INLINE, LINK, DISPLAY, DOWNLOAD, +} \ No newline at end of file diff --git a/contentstack/src/main/java/com/contentstack/sdk/NodeToHTML.java b/contentstack/src/main/java/com/contentstack/sdk/NodeToHTML.java new file mode 100644 index 00000000..e290582a --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/NodeToHTML.java @@ -0,0 +1,45 @@ +package com.contentstack.sdk; + +import org.json.JSONObject; + +public class NodeToHTML { + private NodeToHTML() { + throw new IllegalStateException("Could not create instance of NodeToHTML"); + } + public static String textNodeToHTML(JSONObject nodeText, Option renderOption) { + String text = nodeText.optString("text"); + text = text.replace("\n", "
"); + if (nodeText.has("superscript")) { + text = renderOption.renderMark(MarkType.SUPERSCRIPT, text); + } + if (nodeText.has("subscript")) { + text = renderOption.renderMark(MarkType.SUBSCRIPT, text); + } + if (nodeText.has("inlineCode")) { + text = renderOption.renderMark(MarkType.INLINECODE, text); + } + if (nodeText.has("strikethrough")) { + text = renderOption.renderMark(MarkType.STRIKETHROUGH, text); + } + if (nodeText.has("underline")) { + text = renderOption.renderMark(MarkType.UNDERLINE, text); + } + if (nodeText.has("italic")) { + text = renderOption.renderMark(MarkType.ITALIC, text); + } + if (nodeText.has("bold")) { + text = renderOption.renderMark(MarkType.BOLD, text); + } + if (nodeText.has("break")) { + if (!text.contains("
")) { + text = renderOption.renderMark(MarkType.BREAK, text); + } + // text = renderOption.renderMark(MarkType.BREAK, text); + } + return text; + } +} + +enum MarkType { + BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, INLINECODE, SUBSCRIPT, SUPERSCRIPT, BREAK, +} diff --git a/contentstack/src/main/java/com/contentstack/sdk/Option.java b/contentstack/src/main/java/com/contentstack/sdk/Option.java new file mode 100644 index 00000000..c218597f --- /dev/null +++ b/contentstack/src/main/java/com/contentstack/sdk/Option.java @@ -0,0 +1,24 @@ +package com.contentstack.sdk; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public interface Option { + String renderOptions(JSONObject embeddedObject, Metadata metadata); + String renderMark(MarkType markType, String renderText); + String renderNode(String nodeType, JSONObject nodeObject, NodeCallback callback); +} + +interface NodeCallback { + + /** + * The function takes a JSONArray of nodes and returns a string representation of their children. + * + * @param nodeJsonArray The `nodeJsonArray` parameter is a JSONArray object that contains a + * collection of JSON objects representing nodes. Each JSON object represents a node and contains + * information about the node's properties and children. + * @return The method is returning a String. + */ + String renderChildren(JSONArray nodeJsonArray); +} \ No newline at end of file diff --git a/contentstack/src/main/java/com/contentstack/sdk/SDKUtil.java b/contentstack/src/main/java/com/contentstack/sdk/SDKUtil.java index 460b41a8..acf0723a 100755 --- a/contentstack/src/main/java/com/contentstack/sdk/SDKUtil.java +++ b/contentstack/src/main/java/com/contentstack/sdk/SDKUtil.java @@ -8,7 +8,10 @@ import android.os.SystemClock; import android.util.Log; +import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; +import org.w3c.dom.Attr; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; @@ -19,9 +22,15 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.TimeZone; +import java.util.jar.Attributes; +import java.util.stream.StreamSupport; /** * @author contentstack.com @@ -246,4 +255,190 @@ public static Calendar parseDate(String date, String dateFormat, TimeZone timeZo return cal; } + public static void jsonToHTML(JSONArray entryArray, String[] keyPath, Option option) throws JSONException { + for (int i = 0; i < entryArray.length(); i++) { + Object jsonObj = entryArray.get(i); + jsonToHTML((JSONObject) jsonObj, keyPath, option); + } + } + + public static void jsonToHTML(JSONObject entry, String[] keyPath, Option renderOption) throws JSONException { + MetaToEmbedCallback converter = metadata -> { + boolean available = entry.has("_embedded_items"); + if (available) { + JSONObject jsonArray = entry.optJSONObject("_embedded_items"); + return findEmbeddedItems(jsonArray, metadata); + } + return Optional.empty(); + }; + ContentCallback callback = content -> { + if (content instanceof JSONArray) { + JSONArray contentArray = (JSONArray) content; + return enumerateContents(contentArray, renderOption, converter); + } else if (content instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) content; + return enumerateContent(jsonObject, renderOption, converter); + } + return null; + }; + for (String path: keyPath) { + findContent(entry, path, callback); + } + } + + private static void findContent(JSONObject entryObj, String path, ContentCallback contentCallback) throws JSONException { + String[] arrayString = path.split("\\."); + getContent(arrayString, entryObj, contentCallback); + } + + private static void getContent(String[] arrayString, JSONObject entryObj, ContentCallback contentCallback) throws JSONException { + if (arrayString != null && arrayString.length != 0) { + String path = arrayString[0]; + if (arrayString.length == 1) { + Object varContent = entryObj.opt(path); + if (varContent instanceof String || varContent instanceof JSONArray || varContent instanceof JSONObject) { + entryObj.put(path, contentCallback.contentObject(varContent)); + } + } else { + List list = new ArrayList<>(Arrays.asList(arrayString)); + list.remove(path); + String[] newArrayString = list.toArray(new String[0]); + if (entryObj.opt(path) instanceof JSONObject) { + getContent(newArrayString, entryObj.optJSONObject(path), contentCallback); + } else if (entryObj.opt(path) instanceof JSONArray) { + JSONArray jsonArray = entryObj.optJSONArray(path); + for (int idx = 0; idx < jsonArray.length(); idx++) { + getContent(newArrayString, jsonArray.optJSONObject(idx), contentCallback); + } + } + } + } + } + + private static Optional findEmbeddedItems(JSONObject jsonObject, Metadata metadata) { + Set allKeys = (Set) jsonObject.keys(); + for (String key: allKeys) { + JSONArray jsonArray = jsonObject.optJSONArray(key); + + Optional filteredContent = Optional.empty(); + for (int i = 0; i < jsonArray.length(); i++) { + try { + JSONObject obj = jsonArray.getJSONObject(i); + if (jsonObject.optString("uid").equalsIgnoreCase(metadata.getItemUid())) { + filteredContent = Optional.of(obj); + break; + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + if (filteredContent.isPresent()) { + return filteredContent; + } + } + return Optional.empty(); + } + + private static Object enumerateContents(JSONArray contentArray, Option renderObject, MetaToEmbedCallback item) { + JSONArray jsonArrayRTEContent = new JSONArray(); + for (int i = 0; i < contentArray.length(); i++) { + Object RTE = null; + try { + RTE = contentArray.get(i); + } catch (JSONException e) { + throw new RuntimeException(e); + } + JSONObject jsonObject = (JSONObject) RTE; + String renderContent = enumerateContent(jsonObject, renderObject, item); + jsonArrayRTEContent.put(renderContent); + } + return jsonArrayRTEContent; + } + private static String enumerateContent(JSONObject jsonObject, Option renderObject, MetaToEmbedCallback item) { + if (jsonObject.length() > 0 && jsonObject.has("type") && jsonObject.has("children")) { + if (jsonObject.opt("type").equals("doc")) { + return doRawProcessing(jsonObject.optJSONArray("children"), renderObject, item); + } + } + return ""; + } + private static String doRawProcessing(JSONArray children, Option renderObject, MetaToEmbedCallback embedItem) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < children.length(); i++) { + Object item = null; + try { + item = children.get(i); + } catch (JSONException e) { + throw new RuntimeException(e); + } + JSONObject child; + if (item instanceof JSONObject) { + child = (JSONObject) item; + stringBuilder.append(extractKeys(child, renderObject, embedItem)); + } + } + return stringBuilder.toString(); + } + + private static final String ASSET = "asset"; + private static String extractKeys(JSONObject jsonNode, Option renderObject, MetaToEmbedCallback embedItem) { + if (!jsonNode.has("type") && jsonNode.has("text")) { + return NodeToHTML.textNodeToHTML(jsonNode, renderObject); + } else if (jsonNode.has("type")) { + String nodeType = jsonNode.optString("type"); + if (nodeType.equalsIgnoreCase("reference")) { + JSONObject attrObj = jsonNode.optJSONObject("attrs"); + String attrType = attrObj.optString("type"); + Metadata metadata; + + if (attrType.equalsIgnoreCase(ASSET)) { + String text = attrObj.optString("text"); + String uid = attrObj.optString("asset-uid"); + String style = attrObj.optString("display-type"); + metadata = new Metadata(text, attrType, uid, ASSET, style, "", new Attributes()); + } else { + String text = attrObj.optString("text"); + String uid = attrObj.optString("entry-uid"); + String contentType = attrObj.optString("content-type-uid"); + String style = attrObj.optString("display-type"); + metadata = new Metadata(text, attrType, uid, contentType, style, "", new Attributes()); + } + Optional filteredContent = embedItem.toEmbed(metadata); + if (filteredContent.isPresent()) { + JSONObject contentToPass = filteredContent.get(); + return getStringOption(renderObject, metadata, contentToPass); + } else { + if (attrType.equalsIgnoreCase(ASSET)) { + return renderObject.renderNode("img", jsonNode, nodeJsonArray -> doRawProcessing(nodeJsonArray, renderObject, embedItem)); + } + } + } else { + return renderObject.renderNode(nodeType, jsonNode, nodeJsonArray -> doRawProcessing(nodeJsonArray, renderObject, embedItem)); + } + } + return ""; + } + private static String getStringOption(Option option, Metadata metadata, JSONObject contentToPass) { + String stringOption = option.renderOptions(contentToPass, metadata); + if (stringOption == null) { + DefaultOption defaultOptions = new DefaultOption(); + stringOption = defaultOptions.renderOptions(contentToPass, metadata); + } + return stringOption; + } } + +interface MetaToEmbedCallback { + Optional toEmbed(Metadata metadata); +} +interface ContentCallback { + + /** + * The function contentObject takes an object as input and returns an object as output. + * + * @param content The content parameter is an object that represents the content to be passed to + * the contentObject function. + * @return The contentObject is being returned. + */ + Object contentObject(Object content); +} \ No newline at end of file