com.atinternet.tracker.Builder.java Source code

Java tutorial

Introduction

Here is the source code for com.atinternet.tracker.Builder.java

Source

/*
This SDK is licensed under the MIT license (MIT)
Copyright (c) 2015- Applied Technologies Internet SAS (registration number B 403 261 258 - Trade and Companies Register of Bordeaux  France)
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
 */

package com.atinternet.tracker;

import android.text.TextUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import static com.atinternet.tracker.ParamOption.RelativePosition.last;
import static com.atinternet.tracker.ParamOption.RelativePosition.none;
import static com.atinternet.tracker.Tool.CallbackType;

/**
 * Hit builder
 */
class Builder implements Runnable {

    // String Constants
    private static final String EMPTY_VALUE = "";
    private static final String PERCENT_VALUE = "%";
    private static final String MH_PARAMETER_FORMAT = "%1$s-%2$s-%3$s";
    private static final String MHID_FORMAT = "%02d%02d%02d%d";
    private static final String OPT_OUT = "opt-out";

    //Number of configuration part
    private static final int REFCONFIGCHUNKS = 4;

    // MH parameter max length
    private static final int MH_PARAMETER_MAX_LENGTH = 30;

    // MHERR parameter length
    private static final int MHERR_PARAMETER_LENGTH = 8;

    // hit max length
    private static final int HIT_MAX_LENGTH = 1600;

    // Max of subhits count
    private static final int HIT_MAX_COUNT = 999;

    /**
     * Contains Tracker configuration
     */
    private final Configuration configuration;

    /**
     * Collection to stock persistent parameters
     */
    private final ArrayList<Param> persistentParams;

    /**
     * Collection to stock volatile parameters
     */
    private final ArrayList<Param> volatileParams;

    /**
     * Tracker instance
     */
    private final Tracker tracker;

    /**
     * Init a Hit builder
     *
     * @param tracker Tracker
     */
    public Builder(Tracker tracker) {
        this.tracker = tracker;
        this.configuration = tracker.getConfiguration();
        this.volatileParams = new ArrayList<Param>(tracker.getBuffer().getVolatileParams());
        this.persistentParams = new ArrayList<Param>(tracker.getBuffer().getPersistentParams());
    }

    /**
     * Build the configuration
     *
     * @return String
     */
    private String buildConfiguration() {
        String conf = EMPTY_VALUE;
        int hitConfigChunks = 0;

        boolean isSecure = (Boolean) configuration.get(TrackerConfigurationKeys.SECURE);
        String log = String.valueOf(configuration.get(TrackerConfigurationKeys.LOG));
        String logSecure = String.valueOf(configuration.get(TrackerConfigurationKeys.LOG_SSL));
        String domain = String.valueOf(configuration.get(TrackerConfigurationKeys.DOMAIN));
        String pixelPath = String.valueOf(configuration.get(TrackerConfigurationKeys.PIXEL_PATH));
        String siteID = String.valueOf(configuration.get(TrackerConfigurationKeys.SITE));

        if (isSecure) {
            if (!TextUtils.isEmpty(logSecure)) {
                conf += "https://" + logSecure + ".";
                hitConfigChunks++;
            }
        } else {
            if (!TextUtils.isEmpty(log)) {
                conf += "http://" + log + ".";
                hitConfigChunks++;
            }
        }
        if (!TextUtils.isEmpty(domain)) {
            conf += domain;
            hitConfigChunks++;
        }
        if (!TextUtils.isEmpty(pixelPath)) {
            conf += pixelPath;
            hitConfigChunks++;
        }

        conf += "?s=" + siteID;
        hitConfigChunks++;

        if (hitConfigChunks != REFCONFIGCHUNKS) {
            Tool.executeCallback(tracker.getListener(), CallbackType.error,
                    "There is something wrong with configuration : " + conf);
            conf = EMPTY_VALUE;
        }

        return conf;
    }

    /**
     * Build the hit
     */
    Object[] build() {
        ArrayList<String> prepareHitsList = new ArrayList<String>();
        ArrayList<String> hitsList = new ArrayList<String>();
        Integer countSplitHits = 1;
        int indexError = -1;
        String queryString = "";
        String idClient = "";
        String configuration = buildConfiguration();

        // Calcul pour connaitre la longueur maximum du hit
        String oltParameter = Tool.getTimeStamp().execute();
        int MAX_LENGTH_AVAILABLE = HIT_MAX_LENGTH
                - (configuration.length() + oltParameter.length() + MH_PARAMETER_MAX_LENGTH);

        LinkedHashMap<String, Object[]> dictionary = new LinkedHashMap<String, Object[]>();
        if (!TextUtils.isEmpty(configuration)) {
            dictionary = prepareQuery();
            Set<String> keySet = dictionary.keySet();

            // Outerloop est un label de rfrence si jamais une boucle doit tre interrompue
            outerloop:

            for (String parameterKey : keySet) {
                Param p = (Param) dictionary.get(parameterKey)[0];
                String value = String.valueOf(dictionary.get(parameterKey)[1]);

                // Rcupration de l'idclient pour viter les redirections
                if (parameterKey.equals(Hit.HitParam.UserId.stringValue())) {
                    idClient = value;
                    MAX_LENGTH_AVAILABLE -= idClient.length();
                }
                // Si la valeur du paramtre est trop grande
                if (value.length() > MAX_LENGTH_AVAILABLE) {

                    // Si le paramtre est dcoupable
                    if (Lists.getSliceReadyParams().contains(parameterKey)) {

                        // Rcupration du sparateur, de la cl et la valeur du paramtre courant
                        String separator = p.getOptions() != null ? p.getOptions().getSeparator() : ",";
                        String[] currentParameterString = value.split("=");
                        String currentKey = currentParameterString[0] + "=";
                        String currentValue = currentParameterString[1];

                        // On dcoupe la valeur du paramtre sur son sparateur
                        String[] valuesList = currentValue.split(separator);

                        for (int i = 0; i < valuesList.length; i++) {
                            String currentSplitValue = valuesList[i];

                            // Si la valeur courante est trop grande
                            if (currentSplitValue.length() > MAX_LENGTH_AVAILABLE) {
                                // Erreur : Valeur trop longue non dcoupable
                                indexError = countSplitHits;
                                queryString += currentKey;
                                int currentMaxLength = MAX_LENGTH_AVAILABLE
                                        - (MHERR_PARAMETER_LENGTH + queryString.length());

                                // On cherche la position du dernier % afin d'viter les exceptions au moment de la dcoupe brutale
                                String splitError = currentSplitValue.substring(0, currentMaxLength);
                                int lastIndexOfPercent = splitError.lastIndexOf(PERCENT_VALUE);

                                if ((lastIndexOfPercent > currentMaxLength - 5)
                                        && (lastIndexOfPercent < currentMaxLength)) {
                                    queryString += splitError.substring(0, lastIndexOfPercent);
                                } else {
                                    queryString += splitError;
                                }
                                Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                        "Multihits: Param " + parameterKey + " value still too long after slicing");

                                // On retourne  l'endroit du code o se trouve outerloop
                                break outerloop;
                            }
                            // Sinon si le hit dj construit + la valeur courante est trop grand
                            else if (queryString.length() + currentSplitValue.length() > MAX_LENGTH_AVAILABLE) {
                                // On cr un nouveau tronon
                                countSplitHits++;
                                prepareHitsList.add(queryString);
                                queryString = idClient + currentKey
                                        + (i == 0 ? currentSplitValue : separator + currentSplitValue);
                            }
                            // Sinon, on continue la construction normalement
                            else {
                                queryString += i == 0 ? currentKey + currentSplitValue
                                        : separator + currentSplitValue;
                            }
                        }
                    } else {
                        // Erreur : le paramtre n'est pas dcoupable
                        indexError = countSplitHits;
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Multihits: parameter " + parameterKey + " value not allowed to be sliced");
                        break;
                    }
                }
                // Sinon, si le hit est trop grand, on le dcoupe entre deux paramtres
                else if (queryString.length() + value.length() > MAX_LENGTH_AVAILABLE) {
                    countSplitHits++;
                    prepareHitsList.add(queryString);
                    queryString = idClient + value;
                }
                //Sinon, on ne dcoupe pas
                else {
                    queryString += value;
                }
            }

            // Si un seul hit est construit
            if (countSplitHits == 1) {
                if (indexError == countSplitHits) {
                    hitsList.add(configuration + makeSubQuery("mherr", "1") + queryString);
                } else {
                    hitsList.add(configuration + queryString);
                }
            }
            // Sinon, si le nombre de tronons > 999, erreur
            else if (countSplitHits > HIT_MAX_COUNT) {
                hitsList.add(configuration + makeSubQuery("mherr", "1") + queryString);
                Tool.executeCallback(tracker.getListener(), CallbackType.warning, "Multihits: too much hit parts");
            }
            // Sinon, on ajoute les hits construits  la liste de hits  envoyer
            else {
                prepareHitsList.add(queryString);
                String mhID = mhIdSuffixGenerator();
                for (int i = 0; i < countSplitHits; i++) {
                    String countSplitHitsString = countSplitHits.toString();
                    String mhParameter = makeSubQuery("mh",
                            String.format(MH_PARAMETER_FORMAT,
                                    Tool.formatNumberLength(Integer.toString(i + 1), countSplitHitsString.length()),
                                    countSplitHitsString, mhID));
                    if (indexError == (i + 1)) {
                        hitsList.add(
                                configuration + mhParameter + makeSubQuery("mherr", "1") + prepareHitsList.get(i));
                    } else {
                        hitsList.add(configuration + mhParameter + prepareHitsList.get(i));
                    }
                }
            }
            if (tracker.getListener() != null) {
                String message = "";
                for (String hit : hitsList) {
                    message += hit + "\n";
                }
                Tool.executeCallback(tracker.getListener(), CallbackType.build, message,
                        TrackerListener.HitStatus.Success);
            }
        }

        return new Object[] { hitsList, oltParameter };
    }

    @Override
    public void run() {
        // Rcupration des lments issus du build
        Object[] buildResult = build();
        ArrayList<String> urls = (ArrayList<String>) buildResult[0];
        String oltParameter = (String) buildResult[1];

        // Envoi du(des) hit(s) construit(s)
        for (String url : urls) {
            new Sender(tracker.getListener(), new Hit(url), false, oltParameter).send(true);
        }
    }

    /**
     * Organize the parameters to respect order
     *
     * @param completeBuffer ArrayList<Param>
     * @return ArrayList<Param>
     */
    private ArrayList<Param> organizeParameters(ArrayList<Param> completeBuffer) {
        Param lastParam = null;
        int position;
        boolean findFirst = false;
        boolean findLast = false;
        ArrayList<int[]> indexes;
        ArrayList<Param> params = new ArrayList<Param>();

        Param refstore = getRefOrRefstoreParam(Hit.HitParam.Refstore.stringValue(), completeBuffer);
        Param ref = getRefOrRefstoreParam(Hit.HitParam.Referrer.stringValue(), completeBuffer);

        for (Param param : completeBuffer) {

            ParamOption options = param.getOptions();

            if (options != null) {

                switch (options.getRelativePosition()) {
                case first:
                    // Insertion au dbut
                    if (!findFirst) {
                        params.add(0, param);
                        findFirst = true;
                    } else {
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Found more than one param with relative position set to first");
                        params.add(param);
                    }
                    break;
                case last:
                    // Sauvegarde du dernier paramtre
                    if (!findLast) {
                        lastParam = param;
                        findLast = true;
                    } else {
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Found more than one param with relative position set to last");
                        params.add(param);
                    }
                    break;
                case before:
                    // Insertion d'un paramtre avant celui de rfrence
                    indexes = Tool.findParameterPosition(options.getRelativeParameterKey(), params);
                    position = indexes.isEmpty() ? -1 : indexes.get(indexes.size() - 1)[1];
                    if (position == -1) {
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Relative param with key " + options.getRelativeParameterKey()
                                        + " could not be found. Param will be appended");
                        params.add(param);
                    } else {
                        params.add(position, param);
                    }
                    break;
                case after:
                    // Insertion d'un paramtre aprs celui de rfrence
                    indexes = Tool.findParameterPosition(options.getRelativeParameterKey(), params);
                    position = indexes.isEmpty() ? -1 : indexes.get(indexes.size() - 1)[1];
                    if (position == -1) {
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Relative param with key " + options.getRelativeParameterKey()
                                        + " could not be found. Param will be appended");
                        params.add(param);
                    } else {
                        params.add(position + 1, param);
                    }
                    break;
                default:// Insertion  la suite
                    params.add(param);
                    break;
                }
            } else {
                params.add(param);
            }
        }
        // Insertion du dernier paramtre
        if (lastParam != null) {
            params.add(lastParam);
        }

        // Insertion du refstore si existant
        if (refstore != null) {
            params.add(refstore);
        }

        // Insertion du ref si existant
        if (ref != null) {
            params.add(ref);
        }

        return params;
    }

    /**
     * Prepare the hit queryString
     *
     * @return LinkedHashMap
     */
    private LinkedHashMap<String, Object[]> prepareQuery() {
        LinkedHashMap<String, Object[]> formattedParameters = new LinkedHashMap<String, Object[]>();

        ArrayList<Param> completeBuffer = new ArrayList<Param>() {
            {
                addAll(persistentParams);
                addAll(volatileParams);
            }
        };

        ArrayList<Param> params = organizeParameters(completeBuffer);

        for (Param p : params) {
            String value = p.getValue().execute();
            String key = p.getKey();

            HashMap<String, String> plugins = PluginParam.get(tracker);
            if (plugins.containsKey(key)) {
                String pluginClass = plugins.get(key);
                Plugin plugin = null;
                try {
                    plugin = (Plugin) Class.forName(pluginClass).newInstance();
                    plugin.execute(tracker);
                    value = plugin.getResponse();
                    p.setType(Param.Type.JSON);
                    key = Hit.HitParam.JSON.stringValue();
                } catch (Exception e) {
                    e.printStackTrace();
                    value = null;
                }
            } else if (key.equals(Hit.HitParam.UserId.stringValue())) {
                if (TechnicalContext.doNotTrackEnabled(Tracker.getAppContext())) {
                    value = OPT_OUT;
                } else if (((Boolean) configuration.get(TrackerConfigurationKeys.HASH_USER_ID))) {
                    value = Tool.SHA_256(value);
                }
            }

            if (p.getType() == Param.Type.Closure && Tool.parseJSON(value) != null) {
                p.setType(Param.Type.JSON);
            }

            if (value != null) {
                // Referrer processing
                if (key.equals(Hit.HitParam.Referrer.stringValue())) {

                    value = value.replace("&", "$").replaceAll("[<>]", "");
                }

                if (p.getOptions() != null && p.getOptions().isEncode()) {
                    value = Tool.percentEncode(value);
                    p.getOptions().setSeparator(Tool.percentEncode(p.getOptions().getSeparator()));
                }
                int duplicateParamIndex = -1;
                String duplicateParamKey = null;

                Set<String> keys = formattedParameters.keySet();

                String[] keySet = keys.toArray(new String[keys.size()]);
                int length = keySet.length;
                for (int i = 0; i < length; i++) {
                    if (keySet[i].equals(key)) {
                        duplicateParamIndex = i;
                        duplicateParamKey = key;
                        break;
                    }
                }

                if (duplicateParamIndex != -1) {
                    List<Object[]> values = new ArrayList<Object[]>(formattedParameters.values());
                    Param duplicateParam = (Param) values.get(duplicateParamIndex)[0];
                    String str = ((String) formattedParameters.get(duplicateParamKey)[1]).split("=")[0] + "=";
                    String val = ((String) formattedParameters.get(duplicateParamKey)[1]).split("=")[1];

                    if (p.getType() == Param.Type.JSON) {
                        Object json = Tool.parseJSON(Tool.percentDecode(val));
                        Object newJson = Tool.parseJSON(Tool.percentDecode(value));

                        if (json != null && json instanceof JSONObject) {
                            Map dictionary = Tool.toMap((JSONObject) json);

                            if (newJson instanceof JSONObject) {
                                Map newDictionary = Tool.toMap((JSONObject) newJson);
                                dictionary.putAll(newDictionary);

                                JSONObject jsonData = new JSONObject(dictionary);
                                formattedParameters.put(key, new Object[] { duplicateParam,
                                        makeSubQuery(key, Tool.percentEncode(jsonData.toString())) });
                            } else {
                                Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                        "Couldn't append value to a dictionary");
                            }
                        } else if (json != null && json instanceof JSONArray) {
                            try {
                                ArrayList<Object> array = new ArrayList<Object>();
                                JSONArray jArray = (JSONArray) json;
                                for (int i = 0; i < jArray.length(); i++) {
                                    array.add(jArray.get(i).toString());
                                }
                                if (newJson instanceof JSONArray) {
                                    jArray = (JSONArray) newJson;
                                    for (int i = 0; i < jArray.length(); i++) {
                                        array.add(jArray.get(i).toString());
                                    }
                                    JSONObject jsonData = new JSONObject(array.toString());
                                    formattedParameters.put(key, new Object[] { duplicateParam,
                                            makeSubQuery(key, Tool.percentEncode(jsonData.toString())) });
                                } else {
                                    Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                            "Couldn't append value to an array");
                                }
                            } catch (JSONException e) {
                                Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                        "Couldn't append value to an array");
                            }
                        } else {
                            Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                    "Couldn't append value to a JSON Object");
                        }
                    } else if (duplicateParam.getType() == Param.Type.JSON) {
                        Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                                "Couldn't append value to a JSON Object");
                    } else {
                        formattedParameters.put(key,
                                new Object[] { duplicateParam, str + val + p.getOptions().getSeparator() + value });
                    }
                } else {
                    formattedParameters.put(key, new Object[] { p, makeSubQuery(key, value) });
                }
            }
        }
        return formattedParameters;
    }

    /**
     * Generate mhIDSuffix
     *
     * @return String
     */
    private String mhIdSuffixGenerator() {
        Random random = new Random();
        int randId = random.nextInt(9000000) + 1000000;

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        int second = calendar.get(Calendar.SECOND);

        return String.format(Locale.getDefault(), MHID_FORMAT, hour, minute, second, randId);
    }

    /**
     * Formatting a parameter
     *
     * @param key   String
     * @param value String
     * @return String
     */
    private String makeSubQuery(String key, String value) {
        return "&" + key + "=" + value;
    }

    private Param getRefOrRefstoreParam(String key, ArrayList<Param> completeBuffer) {
        Param ref = null;
        ArrayList<int[]> refParamPositions = Tool.findParameterPosition(key, completeBuffer);
        int indexRef = refParamPositions.isEmpty() ? -1 : refParamPositions.get(refParamPositions.size() - 1)[1];

        if (indexRef != -1) {
            ref = completeBuffer.remove(indexRef);
            if (ref.getOptions() != null && ref.getOptions().getRelativePosition() != last
                    && ref.getOptions().getRelativePosition() != none) {
                Tool.executeCallback(tracker.getListener(), CallbackType.warning,
                        key + "= parameter will be put in last position");
            }
        }

        return ref;
    }
}