org.opendatakit.tables.sms.MsgHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.tables.sms.MsgHandler.java

Source

/*
 * Copyright (C) 2012 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.opendatakit.tables.sms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.opendatakit.common.android.provider.DataTableColumns;
import org.opendatakit.tables.data.ColumnProperties;
import org.opendatakit.tables.data.ColumnType;
import org.opendatakit.tables.data.DataUtil;
import org.opendatakit.tables.data.DbHelper;
import org.opendatakit.tables.data.DbTable;
import org.opendatakit.tables.data.KeyValueStore;
import org.opendatakit.tables.data.Query;
import org.opendatakit.tables.data.Query.Constraint;
import org.opendatakit.tables.data.TableProperties;
import org.opendatakit.tables.data.UserTable;
import org.opendatakit.tables.utils.SecurityUtil;
import org.opendatakit.tables.utils.ShortcutUtil;

import android.util.Log;

/**
 * A class for handling incoming messages.
 */
public class MsgHandler {

    private static final String SPACE_QUESTION_MARK_SYMBOL = " ?";

    private static final String SPACE_TILDE_SYMBOL = " ~";

    private static final String SPACE_SLASH_SYMBOL = " /";

    private static final String SPACE_BANG_SYMBOL = " !";

    private static final String SPACE_GREATER_THAN_SYMBOL = " >";

    private static final String SPACE_LESS_THAN_SYMBOL = " <";

    private static final String SPACE_EQUALS_SYMBOL = " =";

    private static final String SPACE_PLUS_SYMBOL = " +";

    private static final String EMPTY_STRING = "";

    private static final String AT_SYMBOL = "@";

    private static final char SPACE_CHAR = ' ';

    private static final char HASH_SYMBOL_CHAR = '#';

    private static final String PLUS_SYMBOL = "+";

    private static final String PERCENT_SYMBOL = "%";

    private static final String SPACE = " ";

    private enum Type {
        ADD, QUERY
    }

    private static final int DEFAULT_LIMIT = 25;

    private DataUtil du;
    private DbHelper dbh;
    private TableProperties[] dataTps;
    private TableProperties[] scTps;
    private SMSSender smsSender;

    public MsgHandler(DbHelper dbh, SMSSender smsSender) {
        this.du = DataUtil.getDefaultDataUtil();
        this.dbh = dbh;
        this.smsSender = smsSender;
    }

    public boolean handleMessage(String msg, String phoneNum) {
        Log.d("MSGH", "handling message: " + msg);
        if (!checkIsMessage(msg)) {
            Log.d("MSGH", "is not message for ODK Tables");
            return false;
        }
        init();
        msg = standardize(msg);
        Log.d("MSGH", "standardized message is: " + msg);
        TableProperties tp = findTable(msg);
        if (tp == null) {
            return false;
        }
        Type type = determineType(msg);
        if (type == null) {
            return false;
        }
        if (!checkSecurity(msg, phoneNum, tp, type)) {
            return false;
        }
        msg = stripPassword(msg);
        switch (type) {
        case ADD:
            return handleAdd(tp, msg, phoneNum);
        case QUERY:
            return handleQuery(tp, msg, phoneNum);
        default:
            return false;
        }
    }

    private void init() {
        dataTps = TableProperties.getTablePropertiesForDataTables(dbh, KeyValueStore.Type.ACTIVE);
        // TODO: verify that the tables returned are still valid (that columns haven't been removed or renamed)
        scTps = TableProperties.getTablePropertiesForShortcutTables(dbh, KeyValueStore.Type.ACTIVE);
        Log.d("MSGH", "scTps:" + Arrays.toString(scTps));
    }

    private boolean checkIsMessage(String msg) {
        return msg.startsWith(AT_SYMBOL) && (msg.lastIndexOf(SPACE_CHAR) > 0);
    }

    private String standardize(String msg) {
        msg = msg.trim();
        List<String> scNames = new ArrayList<String>();
        List<String> scInputs = new ArrayList<String>();
        List<String> scOutputs = new ArrayList<String>();
        for (TableProperties scTp : scTps) {
            ColumnProperties cpLabel = scTp.getColumnByDisplayName(ShortcutUtil.LABEL_COLUMN_NAME);
            ColumnProperties cpInput = scTp.getColumnByDisplayName(ShortcutUtil.INPUT_COLUMN_NAME);
            ColumnProperties cpOutput = scTp.getColumnByDisplayName(ShortcutUtil.OUTPUT_COLUMN_NAME);
            String[] scCols = new String[] { cpLabel.getElementKey(), cpInput.getElementKey(),
                    cpOutput.getElementKey() };
            DbTable dbt = DbTable.getDbTable(dbh, scTp);
            UserTable table = dbt.getRaw(new Query(dbh, KeyValueStore.Type.ACTIVE, scTp), scCols);
            for (int i = 0; i < table.getNumberOfRows(); i++) {
                scNames.add(table.getData(i, 0));
                scInputs.add(table.getData(i, 1));
                scOutputs.add(table.getData(i, 2));
            }
        }
        int iterCount = 0;
        while (iterCount < scNames.size()) {
            String[] split = msg.split(SPACE);
            String target = split[0].substring(1);
            for (int j = 0; j < scNames.size(); j++) {
                if (!target.equals(scNames.get(j))) {
                    continue;
                }
                String nextMsg = convertByShortcut(msg, scInputs.get(j), scOutputs.get(j));
                if ((nextMsg != null) && checkIsMessage(nextMsg)) {
                    msg = nextMsg.trim();
                    break;
                }
            }
            iterCount++;
        }
        return msg;
    }

    private String convertByShortcut(String msg, String input, String output) {
        msg = msg.substring(msg.indexOf(SPACE_CHAR) + 1);
        Map<String, String> values = new HashMap<String, String>();
        String[] inSplit = input.split(PERCENT_SYMBOL);
        if (!msg.startsWith(inSplit[0])) {
            return null;
        }
        int index = inSplit[0].length();
        for (int i = 1; i < inSplit.length - 1; i += 2) {
            int nextIndex = msg.indexOf(inSplit[i + 1], index);
            if (nextIndex < 0) {
                return null;
            }
            values.put(inSplit[i], msg.substring(index, nextIndex));
            index = nextIndex + inSplit[i + 1].length();
        }
        if (index != msg.length()) {
            values.put(inSplit[inSplit.length - 1], msg.substring(index));
        }
        String[] outSplit = output.split(PERCENT_SYMBOL);
        int start = output.startsWith(PERCENT_SYMBOL) ? 0 : 1;
        for (int i = start; i < outSplit.length; i += 2) {
            String key = outSplit[i];
            if (!values.containsKey(key)) {
                return null;
            }
            outSplit[i] = values.get(key);
        }
        StringBuilder sb = new StringBuilder();
        for (String s : outSplit) {
            sb.append(s);
        }
        return sb.toString();
    }

    private TableProperties findTable(String msg) {
        String target = msg.split(SPACE)[0].substring(1);
        for (TableProperties tp : dataTps) {
            if (tp.getDisplayName().equals(target)) {
                return tp;
            }
        }
        return null;
    }

    private Type determineType(String msg) {
        String[] split = msg.split(SPACE);
        if (split.length < 2) {
            return null;
        }
        if (split[1].startsWith(PLUS_SYMBOL)) {
            return Type.ADD;
        } else {
            return Type.QUERY;
        }
    }

    private boolean checkSecurity(String msg, String phoneNum, TableProperties tp, Type type) {
        String secTableId;
        secTableId = tp.getAccessControls();
        // TODO fix this
        //        if (type == Type.ADD) {
        //            secTableId = tp.getWriteSecurityTableId();
        //        } else {
        //            secTableId = tp.getReadSecurityTableId();
        //        }
        if (secTableId == null) {
            return true;
        }
        String password = EMPTY_STRING;
        int lastHashIndex = msg.lastIndexOf(HASH_SYMBOL_CHAR);
        if ((lastHashIndex > 0) && (msg.length() > lastHashIndex + 2)
                && (msg.charAt(lastHashIndex - 1) == SPACE_CHAR)) {
            password = msg.substring(lastHashIndex + 1);
        }
        DbTable sDbt = DbTable.getDbTable(dbh,
                TableProperties.getTablePropertiesForTable(dbh, secTableId, KeyValueStore.Type.ACTIVE));
        ArrayList<String> columns = new ArrayList<String>();
        columns.add(SecurityUtil.PASSWORD_COLUMN_NAME);
        UserTable table = sDbt.getRaw(columns,
                new String[] { DataTableColumns.SAVEPOINT_TYPE, SecurityUtil.PHONENUM_COLUMN_NAME },
                new String[] { DbTable.SavedStatus.COMPLETE.name(), phoneNum }, null);
        for (int i = 0; i < table.getNumberOfRows(); i++) {
            if (password.equals(table.getData(i, 0))) {
                return true;
            }
        }
        return false;
    }

    private String stripPassword(String msg) {
        int lastSpaceIndex = msg.lastIndexOf(SPACE_CHAR);
        if ((msg.length() > lastSpaceIndex + 1) && (msg.charAt(lastSpaceIndex + 1) == HASH_SYMBOL_CHAR)) {
            return msg.substring(0, lastSpaceIndex).trim();
        }
        return msg;
    }

    private boolean handleAdd(TableProperties tp, String msg, String phoneNum) {
        Map<String, String> values = new HashMap<String, String>();
        int plusIndex = msg.indexOf(SPACE_PLUS_SYMBOL) + 1;
        while (plusIndex > 0) {
            // finding index of space between column and value
            int spaceIndex = msg.indexOf(SPACE_CHAR, plusIndex + 2);
            if (spaceIndex < 0) {
                return false;
            }
            // getting the column name
            String key = msg.substring(plusIndex + 1, spaceIndex).trim();
            int secondSpaceIndex = msg.indexOf(SPACE_CHAR, spaceIndex + 1);
            if (secondSpaceIndex < 0) {
                // we've gotten to the end of the message; the remainder must
                // be the value
                String value = msg.substring(spaceIndex).trim();
                values.put(key, value);
                break;
            }
            plusIndex = msg.indexOf(SPACE_PLUS_SYMBOL, secondSpaceIndex) + 1;
            if (plusIndex < 1) {
                // we got to the end of the message
                String value = msg.substring(spaceIndex).trim();
                values.put(key, value);
                break;
            }
            String value = msg.substring(spaceIndex, plusIndex).trim();
            values.put(key, value);
        }
        Map<String, String> rowValues = new HashMap<String, String>();
        for (String key : values.keySet()) {
            ColumnProperties cp = tp.getColumnByUserLabel(key);
            if (cp == null) {
                return false;
            }
            String value = du.validifyValue(cp, values.get(key));
            if (value == null) {
                return false;
            } else if (cp.getSmsIn()) {
                rowValues.put(cp.getElementKey(), value);
            }
        }
        DbTable dbt = DbTable.getDbTable(dbh, tp);

        String savepointCreator = SecurityUtil.mapPhoneNumToUserIdentity(phoneNum);
        dbt.addRow(null, "_sms_receive_", null /* locale */, System.currentTimeMillis(), savepointCreator,
                rowValues);
        return true;
    }

    private boolean handleQuery(TableProperties tp, String msg, String phoneNum) {
        List<Integer> indices = new ArrayList<Integer>();
        int spaceIndex = msg.indexOf(SPACE_CHAR);
        int charIndex = minNonNegative(msg.indexOf(SPACE_EQUALS_SYMBOL, spaceIndex),
                msg.indexOf(SPACE_LESS_THAN_SYMBOL, spaceIndex), msg.indexOf(SPACE_GREATER_THAN_SYMBOL, spaceIndex),
                msg.indexOf(SPACE_BANG_SYMBOL, spaceIndex), msg.indexOf(SPACE_SLASH_SYMBOL, spaceIndex),
                msg.indexOf(SPACE_TILDE_SYMBOL, spaceIndex), msg.indexOf(SPACE_QUESTION_MARK_SYMBOL, spaceIndex));
        while (charIndex > 0) {
            indices.add(charIndex + 1);
            int index = charIndex + 2;
            charIndex = minNonNegative(msg.indexOf(SPACE_EQUALS_SYMBOL, index),
                    msg.indexOf(SPACE_LESS_THAN_SYMBOL, index), msg.indexOf(SPACE_GREATER_THAN_SYMBOL, index),
                    msg.indexOf(SPACE_BANG_SYMBOL, index), msg.indexOf(SPACE_SLASH_SYMBOL, index),
                    msg.indexOf(SPACE_TILDE_SYMBOL, index), msg.indexOf(SPACE_QUESTION_MARK_SYMBOL, index));
        }
        indices.add(msg.length());
        List<ColumnProperties> cols = new ArrayList<ColumnProperties>();
        Query query = new Query(dbh, KeyValueStore.Type.ACTIVE, tp);
        ColumnProperties drSlotColumn = null;
        int drSlotDuration = 0;
        for (int i = 0; i < indices.size() - 1; i++) {
            String token = msg.substring(indices.get(i), indices.get(i + 1));
            char c = token.charAt(0);
            spaceIndex = token.indexOf(SPACE_CHAR, 2);
            String value = (spaceIndex < 0) ? null : token.substring(spaceIndex).trim();
            String colName = (spaceIndex < 0) ? token.substring(1).trim() : token.substring(1, spaceIndex).trim();
            ColumnProperties cp = tp.getColumnByUserLabel(colName);
            if (cp == null) {
                return false;
            }
            if (c == '?') {
                cols.add(cp);
            } else if (c == '~') {
                if ((value != null) && value.startsWith("d")) {
                    query.setOrderBy(Query.SortOrder.DESCENDING, cp);
                } else {
                    query.setOrderBy(Query.SortOrder.ASCENDING, cp);
                }
            } else if (c == '/') {
                if ((drSlotColumn != null) || (value == null) || (cp.getColumnType() != ColumnType.DATE_RANGE)) {
                    return false;
                }
                drSlotColumn = cp;
                drSlotDuration = du.tryParseDuration(value);
                if (drSlotDuration < 0) {
                    return false;
                }
            } else {
                if (!addConstraint(query, cp, c, value)) {
                    return false;
                }
            }
        }
        if (drSlotColumn == null) {
            return respondToSimpleQuery(phoneNum, tp, cols, query);
        } else {
            return respondToDrSlotQuery(phoneNum, tp, query, drSlotColumn, drSlotDuration);
        }
    }

    private boolean addConstraint(Query query, ColumnProperties cp, char comparator, String value) {
        // TODO: do Time and DateTime get processed the same as Date???
        if ((cp.getColumnType() == ColumnType.DATE_RANGE) || (cp.getColumnType() == ColumnType.DATE)
                || (cp.getColumnType() == ColumnType.DATETIME) || (cp.getColumnType() == ColumnType.TIME)) {
            Interval interval = du.tryParseInterval(value);
            if (interval == null) {
                DateTime dt = du.tryParseInstant(value);
                if (dt == null) {
                    return false;
                }
                String dbValue = du.formatDateTimeForDb(dt);
                if (comparator == '=') {
                    if (cp.getColumnType() == ColumnType.DATE_RANGE) {
                        return false;
                    }
                    query.addConstraint(cp, Query.Comparator.EQUALS, dbValue);
                } else if (comparator == '<') {
                    query.addConstraint(cp, Query.Comparator.LESS_THAN, dbValue);
                } else if (comparator == '>') {
                    query.addConstraint(cp, Query.Comparator.GREATER_THAN, dbValue);
                } else {
                    if (cp.getColumnType() == ColumnType.DATE_RANGE) {
                        return false;
                    }
                    query.addConstraint(cp, Query.Comparator.NOT_EQUALS, dbValue);
                }
            } else {
                if (comparator == '=') {
                    if ((cp.getColumnType() == ColumnType.DATE) || (cp.getColumnType() == ColumnType.DATETIME)
                            || (cp.getColumnType() == ColumnType.TIME)) {
                        query.addConstraint(cp, Query.Comparator.GREATER_THAN_EQUALS,
                                du.formatDateTimeForDb(interval.getStart()));
                        query.addConstraint(cp, Query.Comparator.LESS_THAN,
                                du.formatDateTimeForDb(interval.getEnd()));
                    } else {
                        query.addConstraint(cp, Query.Comparator.EQUALS, du.formatIntervalForDb(interval));
                    }
                } else if (comparator == '<') {
                    query.addConstraint(cp, Query.Comparator.LESS_THAN,
                            du.formatDateTimeForDb(interval.getStart()));
                } else if (comparator == '>') {
                    query.addConstraint(cp, Query.Comparator.GREATER_THAN_EQUALS,
                            du.formatDateTimeForDb(interval.getEnd()));
                } else {
                    if ((cp.getColumnType() == ColumnType.DATE) || (cp.getColumnType() == ColumnType.DATETIME)
                            || (cp.getColumnType() == ColumnType.TIME)) {
                        query.addOrConstraint(cp, Query.Comparator.LESS_THAN,
                                du.formatDateTimeForDb(interval.getStart()), Query.Comparator.GREATER_THAN_EQUALS,
                                du.formatDateTimeForDb(interval.getEnd()));
                    } else {
                        query.addConstraint(cp, Query.Comparator.NOT_EQUALS, du.formatIntervalForDb(interval));
                    }
                }
            }
        } else {
            value = du.validifyValue(cp, value);
            if (value == null) {
                return false;
            }
            if (comparator == '=') {
                query.addConstraint(cp, Query.Comparator.EQUALS, value);
            } else if (comparator == '<') {
                query.addConstraint(cp, Query.Comparator.LESS_THAN, value);
            } else if (comparator == '>') {
                query.addConstraint(cp, Query.Comparator.GREATER_THAN, value);
            } else {
                query.addConstraint(cp, Query.Comparator.NOT_EQUALS, value);
            }
        }
        return true;
    }

    private boolean respondToSimpleQuery(String phoneNum, TableProperties tp, List<ColumnProperties> cols,
            Query query) {
        if (cols.isEmpty()) {
            return false;
        }
        int count = 0;
        for (ColumnProperties cp : cols) {
            if (cp.isUnitOfRetention()) {
                ++count;
            }
        }
        String[] colNames = new String[count];
        count = -1;
        for (ColumnProperties cp : cols) {
            if (cp.isUnitOfRetention()) {
                colNames[++count] = cp.getElementKey();
            }
        }
        DbTable dbt = DbTable.getDbTable(dbh, tp);
        UserTable table = dbt.getRaw(query, colNames);
        String resp;
        if (table.getNumberOfRows() == 0) {
            resp = "No rows found.";
        } else {
            int limit = Math.min(DEFAULT_LIMIT, table.getNumberOfRows());
            List<String> rows = new ArrayList<String>();
            for (int i = 0; i < limit; i++) {
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < cols.size(); j++) {
                    String colName = cols.get(j).getSmsLabel();
                    if (colName == null) {
                        colName = cols.get(j).getDisplayName();
                    }
                    sb.append("," + colName + ":" + table.getData(i, j));
                }
                sb.deleteCharAt(0);
                rows.add(sb.toString());
            }
            StringBuilder sb = new StringBuilder(rows.get(0));
            for (int i = 1; i < rows.size(); i++) {
                sb.append(";" + rows.get(i));
            }
            resp = sb.toString();
        }
        smsSender.sendSMSWithCutoff(phoneNum, resp);
        return true;
    }

    private boolean respondToDrSlotQuery(String phoneNum, TableProperties tp, Query query,
            ColumnProperties drSlotColumn, int drSlotDuration) {
        Set<Constraint> constraints = new HashSet<Constraint>();
        for (int i = query.getConstraintCount(); i >= 0; i--) {
            Constraint c = query.getConstraint(i);
            if (c.getColumnDbName().equals(drSlotColumn.getElementKey())) {
                constraints.add(c);
                query.removeConstraint(i);
            }
        }
        query.setOrderBy(Query.SortOrder.ASCENDING, drSlotColumn);
        DbTable dbt = DbTable.getDbTable(dbh, tp);
        UserTable table = dbt.getRaw(query, new String[] { drSlotColumn.getElementKey() });
        // TODO: range should not be slash-separated but stored as two columns OR json in db...
        List<String[]> rawRanges = new ArrayList<String[]>();
        for (int i = 0; i < table.getNumberOfRows(); i++) {
            rawRanges.add(table.getData(i, 0).split("/"));
        }
        String earlyDate = null;
        String lateDate = null;
        for (int i = 0; i < query.getConstraintCount(); i++) {
            Constraint c = query.getConstraint(i);
            if (c.getComparisonCount() == 2) {
                String[] range;
                if (c.getComparator(0) == Query.Comparator.LESS_THAN) {
                    range = new String[] { c.getValue(0), c.getValue(1) };
                } else {
                    range = new String[] { c.getValue(1), c.getValue(0) };
                }
                boolean inserted = false;
                for (int j = 0; j < rawRanges.size(); j++) {
                    if (range[0].compareTo(rawRanges.get(j)[0]) < 0) {
                        rawRanges.add(j, range);
                        inserted = true;
                        break;
                    }
                }
                if (!inserted) {
                    rawRanges.add(range);
                }
            } else if (c.getComparator(0) == Query.Comparator.LESS_THAN) {
                if (lateDate == null) {
                    lateDate = c.getValue(0);
                } else if (c.getValue(0).compareTo(lateDate) < 0) {
                    lateDate = c.getValue(0);
                }
            } else if (c.getComparator(0) == Query.Comparator.GREATER_THAN) {
                if (earlyDate == null) {
                    earlyDate = c.getValue(0);
                } else if (c.getValue(0).compareTo(earlyDate) > 0) {
                    earlyDate = c.getValue(0);
                }
            }
        }
        if (earlyDate != null) {
            rawRanges.add(new String[] { null, earlyDate });
        }
        if (lateDate != null) {
            for (int j = 0; j < rawRanges.size(); j++) {
                if (lateDate.compareTo(rawRanges.get(j)[0]) < 0) {
                    rawRanges.add(j, new String[] { lateDate, null });
                    break;
                }
            }
        }
        if (rawRanges.isEmpty()) {
            String resp = "anytime";
            smsSender.sendSMSWithCutoff(phoneNum, resp);
            return true;
        }
        List<String[]> ranges = new ArrayList<String[]>();
        ranges.add(rawRanges.get(0));
        for (int i = 1; i < rawRanges.size(); i++) {
            String[] lastRange = ranges.get(ranges.size() - 1);
            String[] nextRange = rawRanges.get(i);
            if (nextRange[1] == null) {
                ranges.add(nextRange);
                break;
            }
            if (nextRange[0].compareTo(lastRange[1]) > 0) {
                ranges.add(nextRange);
            } else if (nextRange[1].compareTo(lastRange[1]) > 0) {
                lastRange[1] = nextRange[1];
            }
        }
        StringBuilder sb = new StringBuilder();
        if (ranges.get(0)[0] != null) {
            DateTime dt = du.parseDateTimeFromDb(ranges.get(0)[0]);
            sb.append(";before" + du.formatShortDateTimeForUser(dt));
        }
        for (int i = 1; i < ranges.size(); i++) {
            DateTime start = du.parseDateTimeFromDb(ranges.get(i - 1)[1]);
            DateTime end = du.parseDateTimeFromDb(ranges.get(i)[0]);
            Duration duration = new Duration(start, end);
            if (duration.getStandardSeconds() >= drSlotDuration) {
                sb.append(";" + du.formatShortDateTimeForUser(start) + "-" + du.formatShortDateTimeForUser(end));
            }
        }
        if (ranges.get(ranges.size() - 1)[1] != null) {
            DateTime dt = du.parseDateTimeFromDb(ranges.get(ranges.size() - 1)[1]);
            sb.append(";after" + du.formatShortDateTimeForUser(dt));
        }
        sb.deleteCharAt(0);
        String resp = sb.toString();
        smsSender.sendSMSWithCutoff(phoneNum, resp);
        return true;
    }

    private int minNonNegative(int... vals) {
        int min = -1;
        for (int val : vals) {
            if (val >= 0) {
                min = (min == -1) ? val : Math.min(min, val);
            }
        }
        return min;
    }
}