diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..87c9c88 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "git.detectSubmodules": true, + "java.project.referencedLibraries": [ + "lib/**/*.jar" + ], + "java.compile.nullAnalysis.mode": "automatic", + "[java]": { + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true, + "editor.trimAutoWhitespace": true + }, + "[xml]": { + "editor.tabSize": 2 + }, + "indentRainbow.excludedLanguages": [ + "java" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index c4a3fc4..cce290f 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,10 @@ -![Icon](assets/icon.png) +#  Dynamic Components for AI2 -# DynamicComponents-AI2 `Extension` +An extension for [MIT App Inventor 2](https://appinventor.mit.edu/) applications that allows to create components dynamically by its name at runtime with blocks. -[![forthebadge](https://forthebadge.com/images/badges/its-not-a-lie-if-you-believe-it.svg)](https://forthebadge.com) +It is based on Java's reflection feature, so this allows us to create instances of classes (components) by its name. Also, unlike other extensions that create components in runtime, this extension doesn't keep a list of all component names because it supports every component which is ever added to your App Inventor distribution by nature. So, not only can you dynamically create common components like `Button`, but you can also create `DatePicker` components. -[![Maintainability](https://api.codeclimate.com/v1/badges/31e4cd31de1bd0e186c8/maintainability)](https://codeclimate.com/github/ysfchn/DynamicComponents-AI2/maintainability) - -Fully supported Dynamic Components extension for MIT App Inventor 2. It is based on Java's reflection feature, so it creates the components by searching for a class by just typing its name. So it doesn't have a limited support for specific components, because it supports every component which is ever added to your App Inventor distribution! - -So if you use Kodular, you will able to create all Kodular components, if you use App Inventor, you will able to create all App Inventor components and so on. Extension components are supported too! - -> ⚠ The `beta` branch will be reset after every release. So stay on the `main` branch if you don't know what you do. - ---- - -### Asynchronous support - -This extension can create components asynchronously or synchronously based on your choice. If you don't want to block the main app during creating a bunch of components, go to the Designer (after importing the extension) and select between "UI" (asynchronous) and "Main" (synchronous). - - +So if you use Kodular, you will able to create all Kodular components, if you use App Inventor, you will able to create all App Inventor components and so on. Creating instances of other extensions are also supported. ## 🧩 Blocks @@ -49,7 +35,7 @@ This extension can create components asynchronously or synchronously based on yo --> - Creates a new dynamic component. It supports all component that added to your current AI2 distribution. + Creates a new dynamic component. It supports all component that added to your current AI2 distribution. Note that you can't create components in Screen directly, you will need to have an arrangement beforehand inside a Screen to do that. componentName parameter can have these values:

@@ -68,6 +54,15 @@ This extension can create components asynchronously or synchronously based on yo
+ + + + + + + Creates a new dynamic component in given container (arrangement/canvas) and return it without saving it to the created components list, so it won't be attached to an ID. Note that you can't create components in Screen directly, you will need to have an arrangement beforehand inside a Screen to do that. + + @@ -131,7 +126,16 @@ This extension can create components asynchronously or synchronously based on yo --> - Removes the component with specified ID from screen/layout and the component list. So you will able to use its ID again as it will be deleted. + Removes the component with specified ID from screen and the component list. So you will able to use its ID again as it will be deleted. + + + + + + + + + Removes a component from the screen. It doesn't need to be created by this extension. But if the given component is dynamically created by this extension, this block will also de-register its ID so its ID can be reused for other components that are going to be created later. @@ -431,6 +435,12 @@ This extension can create components asynchronously or synchronously based on yo +### Asynchronous support + +This extension can create components asynchronously or synchronously based on your choice. If you don't want to block the main app during creating a bunch of components, go to the Designer (after importing the extension) and select between "UI" (asynchronous) and "Main" (synchronous). + + + ## 🔨 Building You will need: @@ -438,7 +448,15 @@ You will need: - Java 1.8 (either OpenJDK or Oracle) - Ant 1.10 or higher -Then execute `ant extensions` in the root of the repository. +After cloning the repository, make sure to fetch submodules first: + +``` +git submodule update --init --recursive +``` + +Then execute `ant extensions` in the root of the repository to build the extension. + +> ⚠ The `beta` branch will be reset after every release. So stay on the `main` branch if you don't know what you do. ## 🏅 License diff --git a/assets/blocks/method_createephemeral.png b/assets/blocks/method_createephemeral.png new file mode 100644 index 0000000..d5e136c Binary files /dev/null and b/assets/blocks/method_createephemeral.png differ diff --git a/assets/blocks/method_removecomponent.png b/assets/blocks/method_removecomponent.png new file mode 100644 index 0000000..e154e7f Binary files /dev/null and b/assets/blocks/method_removecomponent.png differ diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index 1c688f6..0000000 Binary files a/assets/icon.png and /dev/null differ diff --git a/lib/appinventor b/lib/appinventor index 477586b..6380b04 160000 --- a/lib/appinventor +++ b/lib/appinventor @@ -1 +1 @@ -Subproject commit 477586b7e1901db30ed6b8e76cf524c4a05b2802 +Subproject commit 6380b04f69fd0ead8b8b5ca8a06287a020c6c967 diff --git a/src/com/yusufcihan/DynamicComponents/DynamicComponents.java b/src/com/yusufcihan/DynamicComponents/DynamicComponents.java index 17b437e..7e85043 100644 --- a/src/com/yusufcihan/DynamicComponents/DynamicComponents.java +++ b/src/com/yusufcihan/DynamicComponents/DynamicComponents.java @@ -1,8 +1,15 @@ package com.yusufcihan.DynamicComponents; +import com.yusufcihan.DynamicComponents.classes.Utils; +import com.yusufcihan.DynamicComponents.classes.Metadata; + +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.ViewGroup; + import com.google.appinventor.components.annotations.DesignerComponent; import com.google.appinventor.components.annotations.DesignerProperty; -import com.google.appinventor.components.annotations.PropertyCategory; import com.google.appinventor.components.annotations.SimpleEvent; import com.google.appinventor.components.annotations.SimpleFunction; import com.google.appinventor.components.annotations.SimpleObject; @@ -14,158 +21,75 @@ import com.google.appinventor.components.runtime.Component; import com.google.appinventor.components.runtime.ComponentContainer; import com.google.appinventor.components.runtime.EventDispatcher; -import com.google.appinventor.components.runtime.Form; import com.google.appinventor.components.runtime.errors.YailRuntimeError; import com.google.appinventor.components.runtime.util.YailDictionary; import com.google.appinventor.components.runtime.util.YailList; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; -import android.os.Handler; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import gnu.lists.FString; -import gnu.math.DFloNum; -import gnu.math.IntNum; - -import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; -import java.util.Set; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; import java.util.UUID; @DesignerComponent( - description = "Dynamic Components is an extension that creates any component in your App Inventor distribution programmatically, instead of having pre-defined components. Made with ❤️ by Yusuf Cihan.", - category = ComponentCategory.EXTENSION, - helpUrl = "https://github.com/ysfchn/DynamicComponents-AI2/blob/main/README.md", - iconName = "aiwebres/icon.png", - nonVisible = true, - version = 9, - versionName = "2.2.2" + description = + "Create any component available in your App Inventor distribution and create instances of " + + "other extensions programmatically in runtime. Made with ❤️ by Yusuf Cihan.", + category = ComponentCategory.EXTENSION, + helpUrl = "https://github.com/ysfchn/DynamicComponents-AI2/blob/main/README.md", + iconName = "aiwebres/icon.png", + nonVisible = true, + version = 10, + versionName = "2.3.0" ) @SimpleObject(external = true) public class DynamicComponents extends AndroidNonvisibleComponent { - // Base package name for components - private final String BASE = "com.google.appinventor.components.runtime."; + private static final String TAG = Utils.TAG; // Whether component creation should happen on the UI thread private boolean postOnUiThread = false; // Components created with Dynamic Components - private final HashMap COMPONENTS = new HashMap(); + private final HashMap COMPONENTS = new HashMap<>(); // IDs of components created with Dynamic Components - private final HashMap COMPONENT_IDS = new HashMap(); + private final HashMap COMPONENT_IDS = new HashMap<>(); private Object lastUsedId = ""; - private ArrayList componentListeners = new ArrayList(); - private JSONArray propertiesArray = new JSONArray(); - private final Util UTIL_INSTANCE = new Util(); + private final ArrayList componentListeners = new ArrayList<>(); public DynamicComponents(ComponentContainer container) { super(container.$form()); } interface ComponentListener { - public void onCreation(Component component, String id); + void onCreation(Component component, String id); } - class Util { - public boolean exists(Component component) { - return COMPONENTS.containsValue(component); - } - - public boolean exists(String id) { - return COMPONENTS.containsKey(id); - } - - public String getClassName(Object componentName) { - String regex = "[^.$@a-zA-Z0-9]"; - String componentNameString = componentName.toString().replaceAll(regex, ""); - - if (componentName instanceof String && componentNameString.contains(".")) { - return componentNameString; - } else if (componentName instanceof String) { - return BASE + componentNameString; - } else if (componentName instanceof Component) { - return componentName.getClass().getName().replaceAll(regex, ""); - } else { - throw new YailRuntimeError("Component is invalid.", "DynamicComponents"); - } - } - - public Method getMethod(Method[] methods, String name, int parameterCount) { - name = name.replaceAll("[^a-zA-Z0-9]", ""); - for (Method method : methods) { - int methodParameterCount = method.getParameterTypes().length; - if (method.getName().equals(name) && methodParameterCount == parameterCount) { - return method; - } - } - - return null; - } - - public void newInstance(Constructor constructor, String id, AndroidViewComponent input) { - Component mComponent = null; - - try { - mComponent = (Component) constructor.newInstance((ComponentContainer) input); - } catch(Exception e) { - throw new YailRuntimeError(e.getMessage(), "DynamicComponents"); - } finally { - if (!isEmptyOrNull(mComponent)) { - String mComponentClassName = mComponent.getClass().getSimpleName(); - if (mComponentClassName == "ImageSprite" || mComponentClassName == "Sprite") { - Invoke(mComponent, "Initialize", new YailList()); - } - - COMPONENT_IDS.put(mComponent, id); - COMPONENTS.put(id, mComponent); - this.notifyListenersOfCreation(mComponent, id); - ComponentBuilt(mComponent, id, mComponentClassName); - } - } - } - - public void parse(String id, JSONObject json) { - JSONObject data = new JSONObject(json.toString()); - data.remove("components"); - - if (!"".equals(id)) { - data.put("in", id); - } - - propertiesArray.put(data); - - if (json.has("components")) { - for (int i = 0; i < json.getJSONArray("components").length(); i++) { - this.parse(data.optString("id", ""), json.getJSONArray("components").getJSONObject(i)); - } - } - } - - public void notifyListenersOfCreation(Component component, String id) { - for (ComponentListener listener : componentListeners) { - listener.onCreation(component, id); - } - } + public boolean isCreatedComponent(String id) { + return COMPONENTS.containsKey(id); } - public boolean isEmptyOrNull(Object item) { - if (item instanceof String) { - String mItem = item.toString(); - mItem = mItem.replace(" ", ""); - return mItem.isEmpty(); + public void notifyListenersOfCreation(Component component, String id) { + for (ComponentListener listener : componentListeners) { + listener.onCreation(component, id); } + } - return item == null; + private void dispatchEvent(final String name, final Object... parameters) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + EventDispatcher.dispatchEvent(DynamicComponents.this, name, parameters); + } + }); } @DesignerProperty( @@ -175,343 +99,244 @@ public boolean isEmptyOrNull(Object item) { ) @SimpleProperty(userVisible = false) public void Thread(String thread) { - postOnUiThread = (thread == "UI"); + if (thread.equalsIgnoreCase("UI")) { + postOnUiThread = true; + } else if (thread.equalsIgnoreCase("Main")) { + postOnUiThread = false; + } else { + throw new YailRuntimeError("Unexpected value '" + thread + "'", TAG); + } } @SimpleEvent(description = "Is called after a component has been created.") public void ComponentBuilt(final Component component, final String id, final String type) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - EventDispatcher.dispatchEvent(DynamicComponents.this, "ComponentBuilt", component, id, type); - } - }); + dispatchEvent("ComponentBuilt", component, id, type); } @SimpleEvent(description = "Is called after a schema has/mostly finished component creation.") public void SchemaCreated(final String name, final YailList parameters) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - EventDispatcher.dispatchEvent(DynamicComponents.this, "SchemaCreated", name, parameters); - } - }); + dispatchEvent("SchemaCreated", name, parameters); } @SimpleFunction(description = "Assign a new ID to a previously created dynamic component.") public void ChangeId(String id, String newId) { - if (UTIL_INSTANCE.exists(id) && !UTIL_INSTANCE.exists(newId)) { - for (String mId : UsedIDs().toStringArray()) { + if (checkBeforeReplacement(id, newId)) { + for (String mId : COMPONENTS.keySet()) { if (mId.contains(id)) { - Component mComponent = (Component) GetComponent(mId); + Component mComponent = COMPONENTS.get(mId); String mReplacementId = mId.replace(id, newId); COMPONENT_IDS.remove(mComponent); COMPONENTS.put(mReplacementId, COMPONENTS.remove(mId)); COMPONENT_IDS.put(mComponent, mReplacementId); } } - } else { - throw new YailRuntimeError("The ID you used is either not a dynamic component, or the ID you've used to replace the old ID is already taken.", "DynamicComponents"); } } - @SimpleFunction(description = "Creates a new dynamic component.") + @SimpleFunction(description = "Replace an existing ID with a new one.") + public void ReplaceId(String id, String newId) { + if (checkBeforeReplacement(id, newId)) { + final Component component = COMPONENTS.get(id); + COMPONENTS.remove(id); + COMPONENT_IDS.remove(component); + COMPONENTS.put(newId, component); + COMPONENT_IDS.put(component, newId); + } + } + + private boolean checkBeforeReplacement(String id, String newId) { + if (isCreatedComponent(id) && !isCreatedComponent(newId)) { + return true; + } + throw new YailRuntimeError( + "The ID you used is either not a dynamic component, or the ID you've used " + + "to replace the old ID is already taken.", TAG + ); + } + + @SimpleFunction(description = + "Creates a new dynamic component in given container (arrangement/canvas) and assign to an ID to reference " + + "the created component later. The 'ComponentBuilt' event will be invoked when the component has created. " + + "Note that you can't create components in Screen directly, you will need to have an arrangement beforehand " + + "inside a Screen to do that." + ) public void Create(final AndroidViewComponent in, Object componentName, final String id) throws Exception { if (!COMPONENTS.containsKey(id)) { lastUsedId = id; - - String mClassName = UTIL_INSTANCE.getClassName(componentName); - Class mClass = Class.forName(mClassName); + Class mClass = Class.forName(Utils.getClassName(componentName)); final Constructor mConstructor = mClass.getConstructor(ComponentContainer.class); - if (postOnUiThread) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { - UTIL_INSTANCE.newInstance(mConstructor, id, in); + Component mComponent = Utils.createInstance(mConstructor, in); + COMPONENT_IDS.put(mComponent, id); + COMPONENTS.put(id, mComponent); + notifyListenersOfCreation(mComponent, id); + ComponentBuilt(mComponent, id, mComponent.getClass().getSimpleName()); } }); } else { - UTIL_INSTANCE.newInstance(mConstructor, id, in); + Component mComponent = Utils.createInstance(mConstructor, in); + COMPONENT_IDS.put(mComponent, id); + COMPONENTS.put(id, mComponent); + notifyListenersOfCreation(mComponent, id); + ComponentBuilt(mComponent, id, mComponent.getClass().getSimpleName()); } } else { - throw new YailRuntimeError("Expected a unique ID, got '" + id + "'.", "DynamicComponents"); + throw new YailRuntimeError("All component IDs must be unique, the component ID '" + id + "' has already used before.", TAG); } } - @SimpleFunction(description = "Generates a random ID to create a component with.") - public String GenerateID() { - String id = ""; + @SimpleFunction(description = + "Creates a new dynamic component in given container (arrangement/canvas) and return it without saving it to the " + + "created components list, so it won't be attached to an ID. Note that you can't create components " + + "in Screen directly, you will need to have an arrangement beforehand inside a Screen to do that." + ) + public Component CreateEphemeral(final AndroidViewComponent in, Object componentName) throws Exception { + Class mClass = Class.forName(Utils.getClassName(componentName)); + final Constructor mConstructor = mClass.getConstructor(ComponentContainer.class); + return Utils.createInstance(mConstructor, in); + } + @SimpleFunction(description = "Generates a random UUID, can be useful to create components with random ID.") + public String GenerateID() { + String id; do { id = UUID.randomUUID().toString(); - } while (UTIL_INSTANCE.exists(id)); - + } while (isCreatedComponent(id)); return id; } - @SimpleFunction(description = "Returns the component associated with the specified ID.") + @SimpleFunction(description = "Returns the component associated with the specified ID. If not found, returns an empty string.") public Object GetComponent(String id) { - return COMPONENTS.get(id); + Component component = COMPONENTS.get(id); + if (component == null) { + return ""; + } + return component; } @SimpleFunction(description = "Get meta data about the specified component.") public YailDictionary GetComponentMeta(Component component) { - Class mClass = component.getClass(); - DesignerComponent mDesignerAnnotation = mClass.getAnnotation(DesignerComponent.class); - boolean mHasDesigner = !isEmptyOrNull(mDesignerAnnotation); - boolean mHasObject = false; - SimpleObject mObjectAnnotation = mClass.getAnnotation(SimpleObject.class); - YailDictionary mMeta = new YailDictionary(); - mHasObject = !isEmptyOrNull(mObjectAnnotation); - - if (mHasDesigner && mHasObject) { - // Return all metadata - mMeta.put("androidMinSdk", mDesignerAnnotation.androidMinSdk()); - mMeta.put("category", mDesignerAnnotation.category()); - mMeta.put("dateBuilt", mDesignerAnnotation.dateBuilt()); - mMeta.put("description", mDesignerAnnotation.description()); - mMeta.put("designerHelpDescription", mDesignerAnnotation.designerHelpDescription()); - mMeta.put("external", mObjectAnnotation.external()); - mMeta.put("helpUrl", mDesignerAnnotation.helpUrl()); - mMeta.put("iconName", mDesignerAnnotation.iconName()); - mMeta.put("nonVisible", mDesignerAnnotation.nonVisible()); - mMeta.put("package", mClass.getName()); - mMeta.put("showOnPalette", mDesignerAnnotation.showOnPalette()); - mMeta.put("type", mClass.getSimpleName()); - mMeta.put("version", mDesignerAnnotation.version()); - mMeta.put("versionName", mDesignerAnnotation.versionName()); - } else if (!mHasDesigner && mHasObject) { - // Return some amount of metadata even if there is no - // @DesignerComponent annotation provided - mMeta.put("external", mObjectAnnotation.external()); - mMeta.put("package", mClass.getName()); - mMeta.put("type", mClass.getSimpleName()); - } else { - // Return the least amount of metadata if no - // annotation is provided - mMeta.put("package", mClass.getName()); - mMeta.put("type", mClass.getSimpleName()); - } - - return mMeta; + return Metadata.getComponentCommonInfo(component); } @SimpleFunction(description = "Get meta data about events for the specified component.") public YailDictionary GetEventMeta(Component component) { - Method[] mMethods = component.getClass().getMethods(); - YailDictionary mEvents = new YailDictionary(); - - for (Method mMethod : mMethods) { - SimpleEvent mAnnotation = mMethod.getAnnotation(SimpleEvent.class); - boolean mIsDeprecated = !isEmptyOrNull(mMethod.getAnnotation(Deprecated.class)); - String mName = mMethod.getName(); - YailDictionary mEventMeta = new YailDictionary(); - - if (!isEmptyOrNull(mAnnotation)) { - // Return all metadata - mEventMeta.put("description", mAnnotation.description()); - mEventMeta.put("isDeprecated", mIsDeprecated); - mEventMeta.put("userVisible", mAnnotation.userVisible()); - } else { - // Return the least amount of metadata if no - // annotation is provided - mEventMeta.put("isDeprecated", mIsDeprecated); - } - - mEvents.put(mName, mEventMeta); + try { + return Metadata.getComponentAnnotationInfo(component, SimpleEvent.class); + } catch (Exception e) { + String errorMessage = e.getMessage() == null ? e.toString() : e.getMessage(); + throw new YailRuntimeError("Couldn't read the metadata: " + errorMessage, TAG); } - - return mEvents; } @SimpleFunction(description = "Get meta data about functions for the specified component.") public YailDictionary GetFunctionMeta(Component component) { - Method[] mMethods = component.getClass().getMethods(); - YailDictionary mFunctions = new YailDictionary(); - - for (Method mMethod : mMethods) { - SimpleFunction mAnnotation = mMethod.getAnnotation(SimpleFunction.class); - boolean mIsDeprecated = !isEmptyOrNull(mMethod.getAnnotation(Deprecated.class)); - String mName = mMethod.getName(); - YailDictionary mFunctionMeta = new YailDictionary(); - - if (!isEmptyOrNull(mAnnotation)) { - // Return all metadata - mFunctionMeta.put("description", mAnnotation.description()); - mFunctionMeta.put("isDeprecated", mIsDeprecated); - mFunctionMeta.put("userVisible", mAnnotation.userVisible()); - } else { - // Return the least amount of metadata if no - // annotation is provided - mFunctionMeta.put("isDeprecated", mIsDeprecated); - } - - mFunctions.put(mName, mFunctionMeta); + try { + return Metadata.getComponentAnnotationInfo(component, SimpleFunction.class); + } catch (Exception e) { + String errorMessage = e.getMessage() == null ? e.toString() : e.getMessage(); + throw new YailRuntimeError("Couldn't read the metadata: " + errorMessage, TAG); } - - return mFunctions; } - @SimpleFunction(description = "Returns the ID of the specified component.") + @SimpleFunction(description = "Returns the ID of the specified component. If not found, returns an empty string.") public String GetId(Component component) { - if (!isEmptyOrNull(component) || COMPONENT_IDS.containsKey(component)) { - return COMPONENT_IDS.get(component); - } - - return ""; - } - - @Deprecated - @SimpleFunction(description = "Do NOT use this function. Use 'GetComponentMeta' as a replacement.") - public String GetName(Component component) { - return component.getClass().getName(); + return COMPONENT_IDS.getOrDefault(component, ""); } - @SimpleFunction(description = "Returns the position of the specified component according to its parent view. Index begins at one.") + @SimpleFunction(description = + "Returns the position of the specified component according to its parent component. " + + "Indexes begins at one. If there is no parent (which shouldn't happen, as the top-most parent is Screen) " + + "then return zero (0)." + ) public int GetOrder(AndroidViewComponent component) { - View mComponent = (View) component.getView(); - int mIndex = 0; - ViewGroup mParent = (!isEmptyOrNull(mComponent) ? (ViewGroup) mComponent.getParent() : null); + // (non null) + View mComponent = component.getView(); + ViewGroup mParent = (ViewGroup) mComponent.getParent(); - if (!isEmptyOrNull(mComponent) && !isEmptyOrNull(mParent)) { - mIndex = mParent.indexOfChild(mComponent) + 1; + if (Utils.isNotEmptyOrNull(mComponent) && Utils.isNotEmptyOrNull(mParent)) { + return mParent.indexOfChild(mComponent) + 1; } - - return mIndex; + return 0; } - @SimpleFunction(description = "Get a properties value.") + @SimpleFunction(description = + "Get a property value of a component with given property name. The returned value can be " + + "any type of value, but if the property value is null, this block will return an empty string instead so " + + "it can be manipulated and compared with other App Inventor blocks." + ) public Object GetProperty(Component component, String name) { - return Invoke(component, name, YailList.makeEmptyList()); + Object returnedValue = Utils.callMethod(component, name, new Object[] { }); + return returnedValue == null ? "" : returnedValue; } @SimpleFunction(description = "Get meta data about properties for the specified component, including their values.") public YailDictionary GetPropertyMeta(Component component) { - Method[] mMethods = component.getClass().getMethods(); - YailDictionary mProperties = new YailDictionary(); - - for (Method mMethod : mMethods) { - DesignerProperty mDesignerAnnotation = mMethod.getAnnotation(DesignerProperty.class); - boolean mHasDesigner = !isEmptyOrNull(mDesignerAnnotation); - boolean mHasProperty = false; - SimpleProperty mPropertyAnnotation = mMethod.getAnnotation(SimpleProperty.class); - String mName = mMethod.getName(); - YailDictionary mPropertyMeta = new YailDictionary(); - Object mValue = Invoke(component, mName, new YailList()); - mHasProperty = !isEmptyOrNull(mPropertyAnnotation); - - if (mHasProperty) { - mPropertyMeta.put("description", mPropertyAnnotation.description()); - mPropertyMeta.put("category", mPropertyAnnotation.category()); - - if (mHasDesigner) { - YailDictionary mDesignerMeta = new YailDictionary(); - mDesignerMeta.put("defaultValue", mDesignerAnnotation.defaultValue()); - mDesignerMeta.put("editorArgs", mDesignerAnnotation.editorArgs()); - mDesignerMeta.put("editorType", mDesignerAnnotation.editorType()); - mPropertyMeta.put("designer", mDesignerMeta); - } - - mPropertyMeta.put("isDeprecated", (!isEmptyOrNull(mMethod.getAnnotation(Deprecated.class)))); - mPropertyMeta.put("isDesignerProperty", mHasDesigner); - mPropertyMeta.put("userVisible", mPropertyAnnotation.userVisible()); - mPropertyMeta.put("value", mValue); - mProperties.put(mName, mPropertyMeta); - } + try { + return Metadata.getComponentPropertyInfo(component); + } catch (Exception e) { + String errorMessage = e.getMessage() == null ? e.toString() : e.getMessage(); + throw new YailRuntimeError("Couldn't read the metadata: " + errorMessage, TAG); } - - return mProperties; } - @SimpleFunction(description = "Invokes a method with parameters.") + @SimpleFunction(description = + "Calls any method of a component by its name and given parameters, and returns its result. " + + "The returned value can be any type of value, but if the returned value is null, this block will return " + + "an empty string instead so it can be manipulated and compared with other App Inventor blocks." + ) public Object Invoke(Component component, String name, YailList parameters) { - if (!isEmptyOrNull(component)) { - Object mInvokedMethod = null; - Method[] mMethods = component.getClass().getMethods(); - - try { - Object[] mParameters = parameters.toArray(); - Method mMethod = UTIL_INSTANCE.getMethod(mMethods, name, mParameters.length); - - Class[] mRequestedMethodParameters = mMethod.getParameterTypes(); - ArrayList mParametersArrayList = new ArrayList(); - for (int i = 0; i < mRequestedMethodParameters.length; i++) { - if ("int".equals(mRequestedMethodParameters[i].getName())) { - mParametersArrayList.add(Integer.parseInt(mParameters[i].toString())); - } else if ("float".equals(mRequestedMethodParameters[i].getName())) { - mParametersArrayList.add(Float.parseFloat(mParameters[i].toString())); - } else if ("double".equals(mRequestedMethodParameters[i].getName())) { - mParametersArrayList.add(Double.parseDouble(mParameters[i].toString())); - } else if ("java.lang.String".equals(mRequestedMethodParameters[i].getName())) { - mParametersArrayList.add(mParameters[i].toString()); - } else if ("boolean".equals(mRequestedMethodParameters[i].getName())) { - mParametersArrayList.add(Boolean.parseBoolean(mParameters[i].toString())); - } else { - mParametersArrayList.add(mParameters[i]); - } - } - - mInvokedMethod = mMethod.invoke(component, mParametersArrayList.toArray()); - } catch (Exception e) { - throw new YailRuntimeError(e.getMessage(), "DynamicComponents"); - } finally { - if (!isEmptyOrNull(mInvokedMethod)) { - return mInvokedMethod; - } else { - return ""; - } - } - } else { - throw new YailRuntimeError("Component cannot be null.", "DynamicComponents"); - } + Object returnedValue = Utils.callMethod(component, name, parameters.toArray()); + return returnedValue == null ? "" : returnedValue; } - @SimpleFunction(description = "Returns if the specified component was created by the Dynamic Components extension.") + @SimpleFunction(description = "Returns true if the specified component was created by this extension, otherwise false.") public boolean IsDynamic(Component component) { return COMPONENTS.containsValue(component); } - @SimpleFunction(description = "Returns the last used ID.") + @SimpleFunction(description = "Returns the last used ID to create a component.") public Object LastUsedID() { return lastUsedId; } - @Deprecated - @SimpleFunction(description = "Do NOT use this function. Use 'GetComponentMeta', 'GetEventMeta', 'GetFunctionMeta', and 'GetPropertyMeta' as replacements.") - public String ListDetails(Component component) { - return ""; - } - @SimpleFunction(description = "Moves the specified component to the specified view.") public void Move(AndroidViewComponent arrangement, AndroidViewComponent component) { - View mComponent = (View) component.getView(); - ViewGroup mParent = (!isEmptyOrNull(mComponent) ? (ViewGroup) mComponent.getParent() : null); - - mParent.removeView(mComponent); - + View mComponent = component.getView(); + ((ViewGroup) mComponent.getParent()).removeView(mComponent); ViewGroup mArrangement = (ViewGroup) arrangement.getView(); ViewGroup mTarget = (ViewGroup) mArrangement.getChildAt(0); - mTarget.addView(mComponent); } - @Deprecated - @SimpleFunction(description = "Do NOT use this function. Use 'GenerateID' as a replacement.") - public String RandomUUID() { - return GenerateID(); - } - - @SimpleFunction(description = "Removes the component with the specified ID from the layout/screen so the ID can be reused.") + @SimpleFunction(description = + "Removes a component from the screen with its ID. The ID will also be de-registered, " + + "so its ID can be reused for other components that are going to be created later." + ) public void Remove(String id) { Object component = COMPONENTS.get(id); + if (component == null) { + return; + } + RemoveComponent((AndroidViewComponent)component); + COMPONENTS.remove(id); + COMPONENT_IDS.remove(component); + } - if (!isEmptyOrNull(component)) { - try { - Method mMethod = component.getClass().getMethod("getView"); + @SimpleFunction(description = + "Removes a component from the screen. It doesn't need to be created by this extension. " + + "But if the given component is dynamically created by this extension, this block will also " + + "de-register its ID so its ID can be reused for other components that are going to be created later." + ) + public void RemoveComponent(AndroidViewComponent component) { + try { + Method mMethod = Utils.getMethod(component, "getView"); + if (mMethod != null) { final View mComponent = (View) mMethod.invoke(component); final ViewGroup mParent = (ViewGroup) mComponent.getParent(); - if (postOnUiThread) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override @@ -522,127 +347,129 @@ public void run() { } else { mParent.removeView(mComponent); } - } catch (Exception e) { - e.printStackTrace(); } - - COMPONENTS.remove(id); - COMPONENT_IDS.remove(component); + final String[] closeMethods = new String[] { "onPause", "onDestroy" }; + for (String methodName : closeMethods) { + final Method invokeMethod = Utils.getMethod(component, methodName); + if (invokeMethod != null) + invokeMethod.invoke(component); + } + // Also remove the component from component list if + // it has created by DynamicComponents. + Object storedComponentId = COMPONENT_IDS.get(component); + if (storedComponentId != null) { + COMPONENTS.remove(storedComponentId); + COMPONENT_IDS.remove(component); + } + } catch (Exception e) { + e.printStackTrace(); } } - @SimpleFunction(description = "Sets the order of the specified component according to its parent view. Typing zero will move the component to the end, index begins at one.") + @SimpleFunction(description = + "Sets the order of the specified component according to its parent view. " + + "Indexes begins at one, and setting to zero (0) will move the component to the end." + ) public void SetOrder(AndroidViewComponent component, int index) { - index = index - 1; - View mComponent = (View) component.getView(); + View mComponent = component.getView(); ViewGroup mParent = (ViewGroup) mComponent.getParent(); - mParent.removeView(mComponent); - - int mChildCount = mParent.getChildCount(); - int mIndex = (index > mChildCount ? mChildCount : index); - - mParent.addView(mComponent, mIndex); + mParent.addView(mComponent, Math.min(index - 1, mParent.getChildCount())); } - @SimpleFunction(description = "Set a property of the specified component, including those only available from the Designer.") + @SimpleFunction(description = + "Set a property of the specified component by its name, including properties " + + "those only available from the Designer." + ) public void SetProperty(Component component, String name, Object value) { - Invoke(component, name, YailList.makeList(new Object[] { - value - })); + Utils.callMethod(component, name, new Object[] { value }); } - @SimpleFunction(description = "Set multiple properties of the specified component using a dictionary, including those only available from the Designer.") + @SimpleFunction(description = + "Set multiple properties of the specified component using a dictionary, " + + "including those only available from the Designer." + ) public void SetProperties(Component component, YailDictionary properties) throws Exception { - JSONObject mProperties = new JSONObject(properties.toString()); - JSONArray mPropertyNames = mProperties.names(); - - for (int i = 0; i < mProperties.length(); i++) { - String name = mPropertyNames.getString(i); - Object value = mProperties.get(name); - Invoke(component, name, YailList.makeList(new Object[] { value })); + for (Map.Entry pair : properties.entrySet()) { + Utils.callMethod(component, (String)pair.getKey(), new Object[] { pair.getValue() }); } } - @SimpleFunction(description = "Uses a JSON Object to create dynamic components. Templates can also contain parameters that will be replaced with the values which are defined from the parameters list.") + @SimpleFunction(description = + "Create components in bulk with a JSON template. Templates can also contain parameters " + + "that will be replaced with the values which are defined from the parameters list. See " + + "the documentation for more information about using and creating templates." + ) public void Schema(AndroidViewComponent in, final String template, final YailList parameters) throws Exception { JSONObject mScheme = new JSONObject(template); - String newTemplate = template; - - if (!isEmptyOrNull(template) && mScheme.has("components")) { - propertiesArray = new JSONArray(); - JSONArray mKeys = (mScheme.has("keys") ? mScheme.getJSONArray("keys") : null); + if (!mScheme.optString("metadata-version", "").equals("1")) { + throw new YailRuntimeError("Metadata version ('metadata-version' key in JSON) must equal to 1.", TAG); + } - if (!isEmptyOrNull(mKeys) && mKeys.length() == parameters.length() - 1) { - for (int i = 0; i < mKeys.length(); i++) { - String keyPercent = "%" + mKeys.getString(i); - String keyBracket = "{" + mKeys.getString(i) + "}"; - String value = parameters.getString(i).replace("\"", ""); - newTemplate = newTemplate.replace(keyPercent, value); - newTemplate = newTemplate.replace(keyBracket, value); - } + if (Utils.isNotEmptyOrNull(template) && mScheme.has("components")) { + JSONArray mKeys = (mScheme.has("keys") ? mScheme.getJSONArray("keys") : new JSONArray()); + if (mKeys.length() != (parameters.length() - 1)) { + throw new YailRuntimeError( + "Given list of template parameters must contain same amount of items that defined in the schema. " + + "The template expects: " + mKeys.length() + ", but given parameters are: " + (parameters.length() - 1), TAG + ); } + LinkedList formatMapping = new LinkedList(); + for (int i = 0; i < mKeys.length(); i++) { + formatMapping.addLast(new String[] { mKeys.getString(i), parameters.getString(i) }); + } + LinkedList componentsList = Utils.componentTreeToList(mScheme.getJSONArray("components"), formatMapping); - mScheme = new JSONObject(newTemplate); - UTIL_INSTANCE.parse("", mScheme); - propertiesArray.remove(0); - - for (int i = 0; i < propertiesArray.length(); i++) { - if (!propertiesArray.getJSONObject(i).has("id")) { - throw new YailRuntimeError("One or multiple components do not have a specified ID in the template.", "DynamicComponents"); - } - - final JSONObject mJson = propertiesArray.getJSONObject(i); - final String mId = mJson.getString("id"); - AndroidViewComponent mRoot = (!mJson.has("in") ? in : (AndroidViewComponent) GetComponent(mJson.getString("in"))); - final String mType = mJson.getString("type"); + for (final JSONObject child : componentsList) { + final String mId = child.getString("id"); + AndroidViewComponent mRoot = (!child.has("parent") ? in : (AndroidViewComponent) COMPONENTS.get(child.getString("parent"))); + final String mType = child.getString("type"); ComponentListener listener = new ComponentListener() { @Override public void onCreation(Component component, String id) { - if (id == mId && mJson.has("properties")) { - JSONObject mProperties = mJson.getJSONObject("properties"); - JSONArray keys = mProperties.names(); - - for (int k = 0; k < keys.length(); k++) { - Invoke( - (Component) GetComponent(mId), - keys.getString(k), - YailList.makeList(new Object[] { - mProperties.get(keys.getString(k)) - }) - ); + try { + if (Objects.equals(id, mId)) { + JSONObject mProperties = child.getJSONObject("properties"); + JSONArray keys = mProperties.names(); + if (keys != null) { + for (int k = 0; k < keys.length(); k++) { + Utils.callMethod( + COMPONENTS.get(mId), + keys.getString(k), + new Object[] { mProperties.get(keys.getString(k)) } + ); + } + } + componentListeners.remove(this); } - - componentListeners.remove(this); + } catch (JSONException e) { + e.printStackTrace(); } } }; - componentListeners.add(listener); - Create(mRoot, mType, mId); } - SchemaCreated(mScheme.getString("name"), parameters); + SchemaCreated(mScheme.optString("name", ""), parameters); } else { - throw new YailRuntimeError("The template is empty, or is does not have any components.", "DynamicComponents"); + throw new YailRuntimeError("The template is empty, or is does not have any components.", TAG); } } - @SimpleFunction(description = "Returns all IDs of components created with the Dynamic Components extension.") + @SimpleFunction(description = "Returns all IDs of components created with this extension as a list.") public YailList UsedIDs() { - Set mKeys = COMPONENTS.keySet(); - return YailList.makeList(mKeys); + return YailList.makeList(COMPONENTS.keySet()); } - @SimpleProperty(description = "Returns the version of the Dynamic Components extension.") + @SimpleProperty(description = "Returns the version of this extension.") public int Version() { return DynamicComponents.class.getAnnotation(DesignerComponent.class).version(); } - @SimpleProperty(description = "Returns the version name of the Dynamic Components extension.") + @SimpleProperty(description = "Returns the version name of this extension.") public String VersionName() { return DynamicComponents.class.getAnnotation(DesignerComponent.class).versionName(); } diff --git a/src/com/yusufcihan/DynamicComponents/aiwebres/icon.png b/src/com/yusufcihan/DynamicComponents/aiwebres/icon.png index 1c688f6..218fc50 100644 Binary files a/src/com/yusufcihan/DynamicComponents/aiwebres/icon.png and b/src/com/yusufcihan/DynamicComponents/aiwebres/icon.png differ diff --git a/src/com/yusufcihan/DynamicComponents/classes/Metadata.java b/src/com/yusufcihan/DynamicComponents/classes/Metadata.java new file mode 100644 index 0000000..de33c4f --- /dev/null +++ b/src/com/yusufcihan/DynamicComponents/classes/Metadata.java @@ -0,0 +1,108 @@ +package com.yusufcihan.DynamicComponents.classes; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import com.google.appinventor.components.annotations.DesignerComponent; +import com.google.appinventor.components.annotations.DesignerProperty; +import com.google.appinventor.components.annotations.SimpleObject; +import com.google.appinventor.components.annotations.SimpleProperty; +import com.google.appinventor.components.runtime.Component; +import com.google.appinventor.components.runtime.util.YailDictionary; + +public class Metadata { + public static YailDictionary getComponentCommonInfo(Component component) { + Class mClass = component.getClass(); + DesignerComponent mDesignerAnnotation = mClass.getAnnotation(DesignerComponent.class); + SimpleObject mObjectAnnotation = mClass.getAnnotation(SimpleObject.class); + YailDictionary mMeta = new YailDictionary(); + + if ((mDesignerAnnotation != null) && (mObjectAnnotation != null)) { + // Return all metadata + mMeta.put("androidMinSdk", mDesignerAnnotation.androidMinSdk()); + mMeta.put("category", mDesignerAnnotation.category()); + mMeta.put("dateBuilt", mDesignerAnnotation.dateBuilt()); + mMeta.put("description", mDesignerAnnotation.description()); + mMeta.put("designerHelpDescription", mDesignerAnnotation.designerHelpDescription()); + mMeta.put("external", mObjectAnnotation.external()); + mMeta.put("helpUrl", mDesignerAnnotation.helpUrl()); + mMeta.put("iconName", mDesignerAnnotation.iconName()); + mMeta.put("nonVisible", mDesignerAnnotation.nonVisible()); + mMeta.put("package", mClass.getName()); + mMeta.put("showOnPalette", mDesignerAnnotation.showOnPalette()); + mMeta.put("type", mClass.getSimpleName()); + mMeta.put("version", mDesignerAnnotation.version()); + mMeta.put("versionName", mDesignerAnnotation.versionName()); + } else if (!(mDesignerAnnotation != null) && (mObjectAnnotation != null)) { + // Return some amount of metadata even if there is no @DesignerComponent annotation. + mMeta.put("external", mObjectAnnotation.external()); + mMeta.put("package", mClass.getName()); + mMeta.put("type", mClass.getSimpleName()); + } else { + // Return the least amount of metadata if no annotation is provided. + mMeta.put("package", mClass.getName()); + mMeta.put("type", mClass.getSimpleName()); + } + + return mMeta; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static YailDictionary getComponentAnnotationInfo(Component component, Class annotationClass) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + YailDictionary dictionaries = new YailDictionary(); + if (component == null) { + return dictionaries; + } + for (Method method : component.getClass().getMethods()) { + YailDictionary dictionary = new YailDictionary(); + Object annotation = method.getAnnotation(annotationClass); + Class annotationC = annotation.getClass(); + final boolean isDeprecated = method.getAnnotation(Deprecated.class) != null; + final String methodName = method.getName(); + + if (annotation != null) { + dictionary.put("description", annotationC.getMethod("description").invoke(annotation)); + dictionary.put("isDeprecated", isDeprecated); + dictionary.put("userVisible", annotationC.getMethod("userVisible").invoke(annotation)); + } + dictionary.put("isDeprecated", isDeprecated); + dictionaries.put(methodName, dictionary); + } + return dictionaries; + } + + public static YailDictionary getComponentPropertyInfo(Component component) + throws IllegalAccessException, InvocationTargetException { + Method[] mMethods = component.getClass().getMethods(); + YailDictionary mProperties = new YailDictionary(); + + for (Method mMethod : mMethods) { + DesignerProperty mDesignerAnnotation = mMethod.getAnnotation(DesignerProperty.class); + SimpleProperty mPropertyAnnotation = mMethod.getAnnotation(SimpleProperty.class); + YailDictionary mPropertyMeta = new YailDictionary(); + Object mValue = mMethod.invoke(component, new Object[] { }); + + if (mPropertyAnnotation != null) { + mPropertyMeta.put("description", mPropertyAnnotation.description()); + mPropertyMeta.put("category", mPropertyAnnotation.category()); + + if (mDesignerAnnotation != null) { + YailDictionary mDesignerMeta = new YailDictionary(); + mDesignerMeta.put("defaultValue", mDesignerAnnotation.defaultValue()); + mDesignerMeta.put("editorArgs", mDesignerAnnotation.editorArgs()); + mDesignerMeta.put("editorType", mDesignerAnnotation.editorType()); + mPropertyMeta.put("designer", mDesignerMeta); + } + + mPropertyMeta.put("isDeprecated", mMethod.getAnnotation(Deprecated.class) != null); + mPropertyMeta.put("isDesignerProperty", mDesignerAnnotation != null); + mPropertyMeta.put("userVisible", mPropertyAnnotation.userVisible()); + mPropertyMeta.put("value", mValue); + mProperties.put(mMethod.getName(), mPropertyMeta); + } + } + + return mProperties; + } +} diff --git a/src/com/yusufcihan/DynamicComponents/classes/Utils.java b/src/com/yusufcihan/DynamicComponents/classes/Utils.java new file mode 100644 index 0000000..6461dfe --- /dev/null +++ b/src/com/yusufcihan/DynamicComponents/classes/Utils.java @@ -0,0 +1,239 @@ +package com.yusufcihan.DynamicComponents.classes; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONArray; +import org.json.JSONObject; + +import com.google.appinventor.components.runtime.AndroidViewComponent; +import com.google.appinventor.components.runtime.Component; +import com.google.appinventor.components.runtime.errors.YailRuntimeError; + +import android.util.Log; + +public class Utils { + // Extension log tag + public static final String TAG = "DynamicComponents"; + // Base package name for components + private static final String BASE = "com.google.appinventor.components.runtime."; + private static final Pattern classNamePattern = Pattern.compile("[^.$@a-zA-Z0-9_]"); + private static final Pattern methodNamePattern = Pattern.compile("[^a-zA-Z0-9]"); + private static final String[] canvasComponents = {"Ball", "ImageSprite", "Sprite"}; + + public static boolean isNotEmptyOrNull(Object item) { + return item instanceof String ? !((String) item).replace(" ", "").isEmpty() : item != null; + } + + /* + Get class name from a component object, component name, + full class name. + + "Button" -> "com.google.appinventor.components.runtime.Button" + (Component block) -> class name of the component + "com.example.MyComponent" -> leave as-is + */ + public static String getClassName(Object componentName) { + String componentNameString = classNamePattern.matcher(componentName.toString()).replaceAll(""); + if (componentName instanceof String && componentNameString.contains(".")) { + return componentNameString; + } else if (componentName instanceof String) { + return BASE + componentNameString; + } else if (componentName instanceof Component) { + Matcher componentNameResolved = classNamePattern.matcher(componentName.getClass().getName()); + return componentNameResolved.replaceAll(""); + } else { + throw new YailRuntimeError("Component is invalid.", TAG); + } + } + + /* + Create a new instance of component constructor and + add it to the given "input" container. + */ + public static Component createInstance(Constructor constructor, AndroidViewComponent input) { + Component createdComponent = null; + try { + createdComponent = (Component) constructor.newInstance(input); + } catch(Exception e) { + String errorMessage = e.getMessage() == null ? "Unknown error" : e.getMessage(); + throw new YailRuntimeError("Couldn't create an instance: " + errorMessage, TAG); + } + // Canvas components needs to be initialized with invoking "Initialize" method. + String createdComponentClassName = createdComponent.getClass().getSimpleName(); + if (Arrays.asList(canvasComponents).contains(createdComponentClassName)) { + callMethod(createdComponent, "Initialize", new Object[] { }); + } + return createdComponent; + } + + /* + Find a method from list of methods by name and parameter count. + Return null if not found. + */ + public static Method getMethod(Object object, String name, int parameterCount) { + String nameString = methodNamePattern.matcher(name).replaceAll(""); + for (Method method : object.getClass().getMethods()) { + int methodParameterCount = method.getParameterTypes().length; + if (method.getName().equals(nameString) && methodParameterCount == parameterCount) { + return method; + } + } + return null; + } + + /* + Get a method of a object by its name. Return null if not found. + */ + public static Method getMethod(Object object, String name) { + try { + return object.getClass().getMethod(name); + } catch (NoSuchMethodException e) { + Log.e(TAG, "[priority=low] Method not found with name: '" + name + "'"); + } + return null; + } + + /* + Invoke a method of an object by its name and get its return value. + */ + public static Object callMethod(Object object, String name, Object[] parameters) { + if (!isNotEmptyOrNull(object)) { + throw new YailRuntimeError("Component cannot be null.", TAG); + } + try { + Method mMethod = getMethod(object, name, parameters.length); + Class[] mRequestedMethodParameters = mMethod.getParameterTypes(); + + for (int i = 0; i < mRequestedMethodParameters.length; i++) { + final String value = String.valueOf(parameters[i]); + + switch (mRequestedMethodParameters[i].getName()) { + case "int": + parameters[i] = Integer.parseInt(value); + break; + case "float": + parameters[i] = Float.parseFloat(value); + break; + case "double": + parameters[i] = Double.parseDouble(value); + break; + case "java.lang.String": + parameters[i] = value; + break; + case "boolean": + parameters[i] = Boolean.parseBoolean(value); + break; + } + } + Object mInvokedMethod = mMethod.invoke(object, parameters); + return mInvokedMethod; + } catch (InvocationTargetException e) { + String errorMessage = e.getCause().getMessage() == null ? e.getCause().toString() : e.getCause().getMessage(); + throw new YailRuntimeError("Got an error inside the invoke: " + errorMessage, TAG); + } catch (Exception e) { + e.printStackTrace(); + String errorMessage = e.getMessage() == null ? e.toString() : e.getMessage(); + throw new YailRuntimeError("Couldn't invoke: " + errorMessage, TAG); + } + } + + /* + Replace template keys in a string for each given template + keys and their corresponding values. + + "Hello, {name}!" -> "Hello, John!" + + We don't need additional formatting options, so we just + simply replace the text here. + */ + public static String formatTemplateString(String text, final Iterable formatMapping) { + String replacedText = text; + for (String[] formatPair : formatMapping) { + final String formatString = "{" + formatPair[0] + "}"; + if (replacedText.contains(formatString)) { + replacedText = replacedText.replace(formatString, formatPair[1]); + } + } + return replacedText; + } + + /* + Recursively traverse the children of a given component and + return a flattened list with "parent" keys appended representing + the ID of the parent component which the current component + will be created in. + */ + public static LinkedList componentDataToList( + String parentId, JSONObject componentData, final Iterable formatMapping + ) { + LinkedList componentsOutput = new LinkedList(); + JSONObject currentComponent = new JSONObject(); + if (!componentData.has("id") || !componentData.has("type")) { + throw new YailRuntimeError("All components in the schema at least must have an 'id' and 'type'.", TAG); + } + currentComponent.put("type", formatTemplateString(componentData.getString("type"), formatMapping)); + currentComponent.put("id", formatTemplateString(componentData.getString("id"), formatMapping)); + if (!parentId.isEmpty()) { + currentComponent.put("parent", parentId); + } + JSONObject currentProperties = new JSONObject(); + if (componentData.has("properties")) { + final JSONObject propertyObject = componentData.getJSONObject("properties"); + final Iterator propertyObjectKeys = propertyObject.keys(); + while (propertyObjectKeys.hasNext()) { + String key = (String)propertyObjectKeys.next(); + Object value = propertyObject.get(key); + currentProperties.put( + formatTemplateString(key, formatMapping), + value instanceof String ? formatTemplateString((String)value, formatMapping) : value + ); + } + } + currentComponent.put("properties", currentProperties); + componentsOutput.addLast(currentComponent); + if (componentData.has("components")) { + final JSONArray childComponents = componentData.getJSONArray("components"); + for (int i = 0; i < childComponents.length(); i++) { + final JSONObject childObject = childComponents.getJSONObject(i); + final LinkedList childTree = componentDataToList( + formatTemplateString(currentComponent.getString("id"), formatMapping), + childObject, formatMapping + ); + for (JSONObject child : childTree) { + componentsOutput.addLast(child); + } + } + } + return componentsOutput; + } + + /* + Recursively traverse children for each component in given list and + return a flattened list in result. + */ + public static LinkedList componentTreeToList( + JSONArray componentList, final Iterable formatMapping + ) { + LinkedList componentsOutput = new LinkedList(); + try { + for (int i = 0; i < componentList.length(); i++) { + LinkedList childTree = componentDataToList("", componentList.getJSONObject(i), formatMapping); + for (JSONObject child : childTree) { + componentsOutput.addLast(child); + } + } + } catch (Exception e) { + e.printStackTrace(); + String errorMessage = e.getMessage() == null ? e.toString() : e.getMessage(); + throw new YailRuntimeError("Couldn't gather components from schema, reason: " + errorMessage, TAG); + } + return componentsOutput; + } +}