org.opentaps.gwt.messages.GwtLabelsGeneratorContainer.java Source code

Java tutorial

Introduction

Here is the source code for org.opentaps.gwt.messages.GwtLabelsGeneratorContainer.java

Source

/*
 * Copyright (c) Open Source Strategies, Inc.
 *
 * Opentaps is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Opentaps is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Opentaps.  If not, see <http://www.gnu.org/licenses/>.
 */
/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.opentaps.gwt.messages;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.FileUtils;
import org.ofbiz.base.container.Container;
import org.ofbiz.base.container.ContainerException;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.service.ServiceDispatcher;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * Converts Opentaps labels into GWT compatible labels.
 *
 * Import the labels from the properties files specified in "opentaps/opentaps-common/config/LabelConfiguration.properties" (see <code>sources</code>).
 *
 * For each label we try to extract its placeholders in order to generate an equivalent method.
 * eg: <code>cart.quantity_not_positive_number=Quantity requested: ${quantityReq} Quantity available: ${quantityAvail}</code>
 *     generates the following method:
 *     <code>String cart_quantity_not_positive_number(String quantityReq, String quantityAvail)</code>
 *     and the text:
 *     <code>cart_quantity_not_positive_number = Quantity requested: {0} Quantity available: {1}</code>
 *
 * We currently support two kind of placeholders: indexed, and FreeMarker.
 *
 * In order to conform with the GWT message, labels are reformatted as follow:
 * <ul>
 *  <li>the label key is transformed into a Java method name, so any special character is replaced by an underscore.
 *      eg: <code>cart.quantity_not_positive_number</code> => <code>cart_quantity_not_positive_number</code>
 *  <li>placeholders are replaced by indexed placeholders.
 *      eg: <code>Quantity requested: ${quantityReq} Quantity available: ${quantityAvail}</code> => <code>Quantity requested: {0} Quantity available: {1}</code>
 *  <li>single quotes are escaped, being replaced by two single quotes.
 *      eg: <code>Show This Item's Notes</code> => <code>Show This Item''s Notes</code>
 * </ul>
 *
 * Because we may include labels that are already GWT formatted, we assume their key starts with an underscore. As a result
 * the only processing will be to lookup placeholders in the non quoted part of it's text.
 * But note that this should be limited to a couple of labels used in GWT-Ext widget (such as the pager).
 * eg: <code>_exampleOfMessage = Hello {0} '{0} - {1}'</code>
 *     generates the following method:
 *     <code>String exampleOfMessage(String param_0)</code>
 *     and the text:
 *     <code>exampleOfMessage = Hello {0} '{0} - {1}'</code>
 */
public final class GwtLabelsGeneratorContainer implements Container {

    private static final String MODULE = GwtLabelsGeneratorContainer.class.getName();
    @SuppressWarnings("unused")
    private static final String CONTAINER_NAME = "gwtlabels-generator-container";

    // the default locale is the locale of the properties file that do no specify the local, IE: CommonUiLabels.properties
    // so we do not need to generate a CommonuiLabels_{DEFAULT_LOCALE}.properties
    private static final String DEFAULT_LOCALE = "en";

    /** Config file. */
    @SuppressWarnings("unused")
    private String configFile = null;

    static Pattern ftlPlaceholderPattern;
    static Pattern indexedPlaceholderPattern;
    static Pattern fakePlaceholderPattern;
    static {

        // this regex identifies FreeMarker placeholders in the label text
        // eg: Welcome ${firstName} ${lastName}
        // It supports first level recursions like ${amountApplied?currency(${isoCode})}
        //  this matches ${... (?: ${..} ...)? }
        String ftlPlaceholderRegex = "`$`{([^`}`$]+?(?:`$`{[^`}]+?`}[^`}`$]+?)?)`}"; // note: using ` instead of \\ for 'better' readability
        ftlPlaceholderRegex = ftlPlaceholderRegex.replace('`', '\\');
        ftlPlaceholderPattern = Pattern.compile(ftlPlaceholderRegex);

        // this regex identifies indexed placeholders in the label text
        // eg: Welcome {0} {1}
        String indexedPlaceholderRegex = "`{([0-9]+)`}"; // note: using ` instead of \\ for 'better' readability
        indexedPlaceholderRegex = indexedPlaceholderRegex.replace('`', '\\');
        indexedPlaceholderPattern = Pattern.compile(indexedPlaceholderRegex);

        // this regex identifies fake placeholders in the label text, this syntax is used in some rare places
        // eg: Welcome {something}
        // this also match FreeMarker placeholders so we only use it after substituting them by indexed placeholders
        String fakePlaceholderRegex = "`{[^0-9]+`}"; // note: using ` instead of \\ for 'better' readability
        fakePlaceholderRegex = fakePlaceholderRegex.replace('`', '\\');
        fakePlaceholderPattern = Pattern.compile(fakePlaceholderRegex);
    }

    /**
     * Creates a new <code>GwtLabelsGeneratorContainer</code> instance.
     */
    public GwtLabelsGeneratorContainer() {
        super();
    }

    /** {@inheritDoc} */
    public void init(String[] args, String configFile) throws ContainerException {
        this.configFile = configFile;
        // disable job scheduler, JMS listener and startup services
        ServiceDispatcher.enableJM(false);
        ServiceDispatcher.enableJMS(false);
        ServiceDispatcher.enableSvcs(false);

        // parse arguments here if needed
    }

    /** {@inheritDoc} */
    public void stop() throws ContainerException {
    }

    /** {@inheritDoc} */
    public boolean start() throws ContainerException {

        // *** Configuration

        // directory containing the template
        String templatePath = "opentaps/opentaps-common/templates/";
        // template to use
        String templateFile = "BaseGWTUILabel.ftl";

        // the path to the GWT module configuration that defines the target locale, should be common.gwt.xml
        String gwtModuleConfigurationFile = "opentaps/opentaps-common/src/common/org/opentaps/gwt/common/common.gwt.xml";

        // directory where the labels are generated (must end with '/')
        String gwtPropertiesDir = "opentaps/opentaps-common/src/common/org/opentaps/gwt/common/client/messages/";
        // the class name of the GWT message interface to generate in the gwtPropertiesDir
        //  this will generate <gwtPropertiesDir>/<gwtLabelFileName>.java as the interface
        //  <gwtPropertiesDir>/<gwtLabelFileName>.properties as the properties files containing the default label definitions (corresponding to DEFAULT_LOCALE)
        //  <gwtPropertiesDir>/<gwtLabelFileName>_<locale>.properties as the properties files containing the label definitions for other locales specified in the GWT configuration (common.gwt.xml)
        String gwtLabelFileName = "CommonMessages";

        // load the list of UI labels to use as a source
        Properties sources = loadPropertiesFile("opentaps/opentaps-common/config/LabelConfiguration.properties");

        try {

            // read the target locales from the GWT module configuration (common.gwt.xml)
            // by parsing the XML structure
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.parse(new File(gwtModuleConfigurationFile));
            Element rootElement = document.getDocumentElement();
            // each locale is listed as a distinct "extend-property" XML tag, ie: <extend-property name="locale" values="fr"/>
            NodeList list = rootElement.getElementsByTagName("extend-property");
            // the list to store the locale 
            List<String> extendLocales = new ArrayList<String>();
            // loop through all "extend-property tags
            for (int i = 0; i < list.getLength(); i++) {
                Element element = (Element) list.item(i);
                // check if the tag defines a locale
                if (element.getAttribute("name").equals("locale")) {
                    // each tag only defines one locale
                    String locale = element.getAttribute("values");
                    if (!extendLocales.contains(locale) && !DEFAULT_LOCALE.equals(locale)) {
                        extendLocales.add(locale);
                    }
                }
            }

            // use the default label definitions to generate the Java interface file
            TreeMap<String, GwtLabelDefinition> defaultGwtLabelDefinitionsMaps = new TreeMap<String, GwtLabelDefinition>();
            Set<Entry<Object, Object>> entries = sources.entrySet();
            for (Entry<Object, Object> entry : entries) {
                try {
                    String fileName = (String) entry.getValue();
                    readLabelsFromSourcePropertiesFile(fileName, DEFAULT_LOCALE, defaultGwtLabelDefinitionsMaps);
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                }
            }

            // render the GWT message interface Java file from the FTL template
            Writer writer = new StringWriter();
            Configuration ftlConfig = new Configuration();
            ftlConfig.setDirectoryForTemplateLoading(new File(templatePath));
            Template t = ftlConfig.getTemplate(templateFile);
            Map<String, Object> context = new HashMap<String, Object>();
            // label keys are sorted alphabetically by the TreeMap, so the values() collection is sorted automatically
            context.put("functions", defaultGwtLabelDefinitionsMaps.values());
            t.process(context, writer);

            // write the GWT message interface Java file
            File javaFile = new File(gwtPropertiesDir + gwtLabelFileName + ".java");
            FileUtils.writeStringToFile(javaFile, writer.toString(), "UTF-8");

            // write the properties files
            writeGwtLocaleProperties(defaultGwtLabelDefinitionsMaps, DEFAULT_LOCALE, gwtPropertiesDir,
                    gwtLabelFileName);
            for (String locale : extendLocales) {
                // iterator all locale and write gwt labels properties file
                writeGwtLocaleProperties(defaultGwtLabelDefinitionsMaps, locale, gwtPropertiesDir,
                        gwtLabelFileName);
            }

        } catch (Exception e) {
            Debug.logError(e, MODULE);
            return false;
        }
        return true;
    }

    /**
     * Writes gwt locale properties, generate locale properties files base default GWT label properties
     * @param defaultGwtLabelDefinitionsMaps a <code>Map</code> of gwt label definitions
     * @param locale a <code>String</code> value of Locale
     * @param gwtPropertiesDir a <code>String</code> value
     * @param gwtLabelFileName a <code>String</code> value
     */
    private void writeGwtLocaleProperties(TreeMap<String, GwtLabelDefinition> defaultGwtLabelDefinitionsMaps,
            String locale, String gwtPropertiesDir, String gwtLabelFileName) throws IOException {
        // write the localized properties files
        StringBuffer stringBuffer = new StringBuffer();
        for (String key : defaultGwtLabelDefinitionsMaps.keySet()) {
            // check the label has the same signature as the default label
            GwtLabelDefinition defaultLabel = defaultGwtLabelDefinitionsMaps.get(key);
            String localizedString = UtilProperties.getMessage(
                    getResourceNameFromPath(defaultLabel.getPropertiesFile()), defaultLabel.getOriginKey(),
                    new Locale(locale));
            if (UtilValidate.isNotEmpty(localizedString)) {
                if (!key.startsWith("_")) {
                    // save the list of parameters that were parsed from the label text
                    // and that will be used as the message interface method parameters
                    List<String> parameters = new ArrayList<String>();
                    localizedString = formatGwtLabel(localizedString, parameters);
                    // replace extra quote character which add by UtilProperties
                    localizedString = localizedString.replace("''{", "'{");
                    localizedString = localizedString.replace("}''", "}'");
                    localizedString = localizedString.replace("'''{", "''{");
                    localizedString = localizedString.replace("}'''", "}''");
                }
                stringBuffer.append(key).append(" = ").append(localizedString).append("\n");
            }
        }
        String outGwtLabelPropertiesFile = gwtPropertiesDir + gwtLabelFileName + ".properties";
        if (!locale.equals(DEFAULT_LOCALE)) {
            outGwtLabelPropertiesFile = gwtPropertiesDir + gwtLabelFileName + "_" + locale + ".properties";
        }
        File propertiesFile = new File(outGwtLabelPropertiesFile);
        FileUtils.writeStringToFile(propertiesFile, stringBuffer.toString(), "UTF-8");
    }

    /**
     * Gets resource name from file path.
     * @param path the path of properties file
     * @return the resource name
     */
    private String getResourceNameFromPath(String path) {
        int beginIndex = path.lastIndexOf("/") < 0 ? 0 : path.lastIndexOf("/") + 1;
        int endIndex = path.lastIndexOf(".") < 0 ? path.length() : path.lastIndexOf(".");
        return path.substring(beginIndex, endIndex);
    }

    /**
     * Reads a properties file into a <code>Properties</code> object using the DEFAULT_LOCALE.
     * @param fileName the properties file name
     * @return the corresponding <code>Properties</code> object
     */
    private static Properties loadPropertiesFile(String fileName) {
        return loadPropertiesFile(fileName, DEFAULT_LOCALE);
    }

    /**
     * Reads a properties file into a <code>Properties</code> object.
     * @param fileName the properties file name
     * @param locale the locale that properties file use
     * @return the corresponding <code>Properties</code> object
     */
    private static Properties loadPropertiesFile(String fileName, String locale) {
        Properties props = new Properties();
        String propertiesFileName = getPropertiesFileName(fileName, locale);
        try {
            File file = new File(propertiesFileName);
            if (file.exists()) {
                InputStream in = new BufferedInputStream(new FileInputStream(file));
                props.load(in);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return props;
    }

    /**
     * Gets locale properties file name.
     * @param fileName the properties file name
     * @param locale the locale that properties file use
     * @return the locale properties file name
     */
    private static String getPropertiesFileName(String fileName, String locale) {
        String propertiesFileName = fileName;
        if (!locale.equals(DEFAULT_LOCALE)) {
            propertiesFileName = propertiesFileName.substring(0, propertiesFileName.indexOf(".properties")) + "_"
                    + locale + ".properties";
        }
        return propertiesFileName;
    }

    /**
     * Reads a source properties file, and adds all its labels.
     * @param fileName the properties file name to load
     * @param functionNames a <code>List</code> of label names
     * @param gwtLabelDefinitionsMap a <code>Map</code> of label names to <code>GwtLabelDefinition</code>
     * @param locale the locale that properties file use
     * @exception IOException if an error occurs
     * @exception FileNotFoundException if an error occurs
     */
    private static void readLabelsFromSourcePropertiesFile(String fileName, String locale,
            Map<String, GwtLabelDefinition> gwtLabelDefinitionsMap) throws IOException, FileNotFoundException {
        // load the file
        Properties properties = null;
        String propertiesFileName = null;
        if (fileName.endsWith(".xml")) {
            // read labels from xml file
            properties = UtilProperties.xmlToProperties(new FileInputStream(fileName), new Locale(locale), null);
            propertiesFileName = fileName;
        } else {
            properties = loadPropertiesFile(fileName, locale);
            propertiesFileName = getPropertiesFileName(fileName, locale);
        }
        if (properties == null) {
            Debug.logError("Null properties for fileName [" + fileName + "] and locale [" + locale + "]", MODULE);
            return;
        }

        // loop over each labels from the loaded file
        Set<Entry<Object, Object>> entries = properties.entrySet();
        for (Entry<Object, Object> entry : entries) {

            try {
                String originKey = (String) entry.getKey();
                String labelKey = (String) entry.getKey();
                String labelText = (String) entry.getValue();

                // Note on GWT formatted labels:
                //  since some labels may need to in GWT format already, we distinguish them from other label in the properties file by
                //  having their key starts with an underscore.
                //  GWT formatted labels may contain indexed placeholder in quoted string for which the parameters must not appear
                //  in the message interface as they are substituted dynamically in the widgets code.
                //  they do not need to be sanitized and can be imported as-is.
                //  for those label we only check normal non-quoted GWT placeholders in order to generate the corresponding message interface parameters
                boolean isGwtFormatted = false;

                // check if the key starts with an underscore and we consider it as already GWT formatted
                if (labelKey.startsWith("_")) {
                    labelKey = labelKey.substring(1);
                    isGwtFormatted = true;
                } else {
                    labelKey = makeValidLabelKey(labelKey);
                }

                // if a label with the same key was already loaded from a different file
                //  just log the duplicate
                if (gwtLabelDefinitionsMap.containsKey(labelKey)) {
                    Debug.logError("Found duplicate label key [" + labelKey + "] in " + propertiesFileName, MODULE);
                    Debug.logError("key [" + labelKey + "] already defined in "
                            + gwtLabelDefinitionsMap.get(labelKey).getPropertiesFile(), MODULE);
                    continue;
                }

                // save the list of parameters that were parsed from the label text
                // and that will be used as the message interface method parameters
                List<String> parameters = new ArrayList<String>();

                // keep track of the first numeric index available
                // this is incremented if we find indexed parameters in the label text so that they cannot conflict with FreeMarker
                // placeholders when they get indexed
                int indexStart = 0;

                if (isGwtFormatted) {
                    // loop through the un-quoted part of the label text
                    String[] quotedTexts = labelText.split("'");
                    for (int i = 0; i < quotedTexts.length; i += 2) {
                        // find each indexed placeholder the text string
                        indexStart += parseIndexedPlaceholders(quotedTexts[i], indexedPlaceholderPattern,
                                parameters);
                    }
                } else {
                    labelText = formatGwtLabel(labelText, parameters);
                }

                // trim the label text
                labelText = labelText.trim();

                // loop through the found placeholders and make the corresponding GwtLabelDefinition objects
                if (parameters.size() > 0) {
                    gwtLabelDefinitionsMap.put(labelKey,
                            new GwtLabelDefinition(propertiesFileName, labelKey, originKey, labelText, parameters));
                } else {
                    gwtLabelDefinitionsMap.put(labelKey,
                            new GwtLabelDefinition(propertiesFileName, labelKey, originKey, labelText));
                }

            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Format gwt label function
     * @param origin a <code>String</code> value
     * @return formatted gwt label
     */
    private static String formatGwtLabel(String origin, List<String> parameters) {
        String labelText = origin;

        // keep track of the first numeric index available
        // this is incremented if we find indexed parameters in the label text so that they cannot conflict with FreeMarker
        // placeholders when they get indexed
        int indexStart = 0;
        // find each indexed placeholder the text string
        indexStart += parseIndexedPlaceholders(labelText, indexedPlaceholderPattern, parameters);

        // find empty freemarker placeholders (actually used in very few labels) and index them
        while (labelText.contains("${}")) {
            labelText = labelText.replaceFirst("\\$\\{\\}", "{" + indexStart + "}");
            indexStart++;
        }

        // save the list of FreeMarker placeholders strings
        // we will replace them by indexed parameters
        List<String> vars = new ArrayList<String>();

        // find each FreeMarker placeholder in the text string
        Matcher matchFtlPlaceholder = ftlPlaceholderPattern.matcher(labelText);
        while (matchFtlPlaceholder.find()) {
            // get the placeholder from the regex
            // eg: ${firstName}
            String placeholder = matchFtlPlaceholder.group(); // ${firstName}
            String placeholderName = matchFtlPlaceholder.group(1); // firstName

            // check the raw placeholder content against what we already parsed
            if (vars.contains(placeholder)) {
                continue;
            }

            // sanitize the placeholderName so that it can be used as a Java variable name
            //  handle special FTL like amountApplied?currency(${isoCode})
            Matcher matchFtlInnerPlaceholder = ftlPlaceholderPattern.matcher(placeholderName);
            if (matchFtlInnerPlaceholder.find()) {
                placeholderName = matchFtlInnerPlaceholder.group(1);
            }

            // in case of two level recursions, or special FTL construct that we did not handle
            // we need to cut the string at the first ? char (used in FTL to check the variable, like in ${firstName?default("Joe")}
            int idx = placeholderName.indexOf("?");
            if (idx > 0) {
                placeholderName = placeholderName.substring(0, idx);
            }

            // some FTL constructs use ${variable()}
            idx = placeholderName.indexOf("(");
            if (idx > 0) {
                placeholderName = placeholderName.substring(0, idx);
            }

            // remove characters not allowed in Java but used in FTL to access object attributes
            // eg: The order ${order.orderId} has been shipped.
            placeholderName = placeholderName.replace('.', '_');

            // check that we did not accidentally make a duplicate
            // else rename it to placeholderName + index
            // eg: firstName1, firstName2 ...
            if (parameters.contains(placeholderName)) {
                String placeholderNameOrig = placeholderName;
                int i = 1;
                while (parameters.contains(placeholderName)) {
                    placeholderName = placeholderNameOrig + i;
                    i++;
                }
            }

            // add the placeholder to the list of parameters for the message interface
            vars.add(placeholder);
            parameters.add(placeholderName);
        }

        // replace the FTL placeholders by GWT placeholders, note that this supports the case where
        //  the same placeholder is used multiple times
        // eg: Welcome ${firstName} ${lastName} ${firstName}  => Welcome {0} {1} {0}
        for (int i = 0; i < vars.size(); i++) {
            labelText = labelText.replace(vars.get(i), "{" + i + "}");
        }

        // find remaining fake placeholders in the text string
        Matcher matchFakePlaceholder = fakePlaceholderPattern.matcher(labelText);
        vars.clear();
        while (matchFakePlaceholder.find()) {
            String placeholder = matchFakePlaceholder.group();

            if (vars.contains(placeholder)) {
                continue;
            }

            vars.add(placeholder);
        }

        // alter fake placeholders
        // eg: {some text} => [some text]
        for (String s : vars) {
            labelText = labelText.replace(s, "[" + s.substring(1, s.length() - 1) + "]");
        }

        // sanitize white space chars like \n \r \t ...
        labelText = labelText.replace('\n', ' ');
        labelText = labelText.replace('\t', ' ');
        labelText = labelText.replace("\r", "");

        // in GWT labels quotes need to be doubled, quoted text has a special meaning.
        labelText = labelText.replace("'", "''");

        return labelText.trim();
    }

    private static String makeValidLabelKey(String labelKey) {
        // enforce a convention that the first character of the label key is lower case
        labelKey = labelKey.substring(0, 1).toLowerCase() + labelKey.substring(1);

        // convert characters that are invalid in Java into underscores so that we can use the label key as a Java method name properly
        labelKey = labelKey.replace('.', '_');
        labelKey = labelKey.replace('-', '_');
        labelKey = labelKey.replace('+', '_');
        labelKey = labelKey.replace('/', '_');
        labelKey = labelKey.replace(' ', '_');

        return labelKey;
    }

    private static int parseIndexedPlaceholders(String labelText, Pattern indexedPlaceholderPattern,
            List<String> parameters) {
        int counter = 0;
        Matcher matchIndexedPlaceholder = indexedPlaceholderPattern.matcher(labelText);
        while (matchIndexedPlaceholder.find()) {

            String placeholder = matchIndexedPlaceholder.group(1); // get the index: 0, 1, ...
            String placeholderName = "param_" + placeholder; // give it a name to be used as the interface method parameter

            // check that it was not already parsed
            if (parameters.contains(placeholderName)) {
                continue;
            }

            // add the placeholder to the list of parameters for the message interface
            parameters.add(placeholderName);
            // increment the counter of indexed parameter
            counter++;
        }
        return counter;
    }

}