/*
* Copyright (c) 1998-2010 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source 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, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.quercus.lib.db;
import com.caucho.quercus.UnimplementedException;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.annotation.ReturnNullAsFalse;
import com.caucho.quercus.env.ArrayValue;
import com.caucho.quercus.env.ArrayValueImpl;
import com.caucho.quercus.env.BooleanValue;
import com.caucho.quercus.env.Env;
import com.caucho.quercus.env.EnvCleanup;
import com.caucho.quercus.env.LongValue;
import com.caucho.quercus.env.StringValue;
import com.caucho.quercus.env.Value;
import com.caucho.util.L10N;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* PDO object oriented API facade.
*/
public class PDO implements EnvCleanup {
private static final Logger log = Logger.getLogger(PDO.class.getName());
private static final L10N L = new L10N(PDO.class);
public static final int ATTR_AUTOCOMMIT = 0;
public static final int ATTR_PREFETCH = 1;
public static final int ATTR_TIMEOUT = 2;
public static final int ATTR_ERRMODE = 3;
public static final int ATTR_SERVER_VERSION = 4;
public static final int ATTR_CLIENT_VERSION = 5;
public static final int ATTR_SERVER_INFO = 6;
public static final int ATTR_CONNECTION_STATUS = 7;
public static final int ATTR_CASE = 8;
public static final int ATTR_CURSOR_NAME = 9;
public static final int ATTR_CURSOR = 10;
public static final int ATTR_ORACLE_NULLS = 11;
public static final int ATTR_PERSISTENT = 12;
public static final int ATTR_STATEMENT_CLASS = 13;
public static final int ATTR_FETCH_TABLE_NAMES = 14;
public static final int ATTR_FETCH_CATALOG_NAMES = 15;
public static final int ATTR_DRIVER_NAME = 16;
public static final int ATTR_STRINGIFY_FETCHES = 17;
public static final int ATTR_MAX_COLUMN_LEN = 18;
public static final int CASE_NATURAL = 0;
public static final int CASE_UPPER = 1;
public static final int CASE_LOWER = 2;
public static final int CURSOR_FWDONLY = 0;
public static final int CURSOR_SCROLL = 1;
public static final String ERR_NONE = "00000";
public static final int ERRMODE_SILENT = 0;
public static final int ERRMODE_WARNING = 1;
public static final int ERRMODE_EXCEPTION = 2;
public static final int FETCH_LAZY = 1;
public static final int FETCH_ASSOC = 2;
public static final int FETCH_NUM = 3;
public static final int FETCH_BOTH = 4;
public static final int FETCH_OBJ = 5;
public static final int FETCH_BOUND = 6;
public static final int FETCH_COLUMN = 7;
public static final int FETCH_CLASS = 8;
public static final int FETCH_INTO = 9;
public static final int FETCH_FUNC = 10;
public static final int FETCH_NAMED = 11;
public static final int FETCH_GROUP = 0x00010000;
public static final int FETCH_UNIQUE = 0x00030000;
public static final int FETCH_CLASSTYPE = 0x00040000;
public static final int FETCH_SERIALIZE = 0x00080000;
public static final int FETCH_ORI_NEXT = 0;
public static final int FETCH_ORI_PRIOR = 1;
public static final int FETCH_ORI_FIRST = 2;
public static final int FETCH_ORI_LAST = 3;
public static final int FETCH_ORI_ABS = 4;
public static final int FETCH_ORI_REL = 5;
public static final int NULL_NATURAL = 0;
public static final int NULL_EMPTY_STRING = 1;
public static final int NULL_TO_STRING = 2;
public static final int PARAM_NULL = 0;
public static final int PARAM_INT = 1;
public static final int PARAM_STR = 2;
public static final int PARAM_LOB = 3;
public static final int PARAM_STMT = 4;
public static final int PARAM_BOOL = 5;
public static final int PARAM_INPUT_OUTPUT = 0x80000000;
// MySQL-specific constants
public static final int MYSQL_ATTR_USE_BUFFERED_QUERY = 1000;
public static final int MYSQL_ATTR_LOCAL_INFILE = 1001;
public static final int MYSQL_ATTR_INIT_COMMAND = 1002;
public static final int MYSQL_ATTR_READ_DEFAULT_FILE = 1003;
public static final int MYSQL_ATTR_READ_DEFAULT_GROUP = 1004;
public static final int MYSQL_ATTR_MAX_BUFFER_SIZE = 1005;
public static final int MYSQL_ATTR_COMPRESS = 1006;
public static final int MYSQL_ATTR_DIRECT_QUERY = 1007;
public static final int MYSQL_ATTR_FOUND_ROWS = 1008;
public static final int MYSQL_ATTR_IGNORE_SPACE = 1009;
public static final Value MYSQL_ATTR_INIT_COMMAND_V = StringValue.create(MYSQL_ATTR_INIT_COMMAND);
public static final Value ATTR_ERRMODE_V = StringValue.create(ATTR_ERRMODE);
private final Env _env;
private final String _dsn;
private String _user;
private String _password;
private final PDOError _error;
private Connection _conn;
private Statement _lastStatement;
private PDOStatement _lastPDOStatement;
private String _lastInsertId;
private boolean _inTransaction;
private static String ENCODING = "ISO8859_1";
private final static Properties driverList = new Properties();
static {
try {
InputStream is = PDO.class.getResourceAsStream("pdo.properties");
driverList.load(is);
is.close();
} catch (Exception e) {
}
}
public PDO(Env env,
String dsn,
@Optional("null") String user,
@Optional("null") String password,
@Optional ArrayValue options) {
_env = env;
_dsn = dsn;
_user = user;
_password = password;
_error = new PDOError(_env);
// TODO: following would be better as annotation on destroy() method
_env.addCleanup(this);
try {
DataSource ds = getDataSource(env, dsn);
if (ds == null) {
env.warning(L.l("'{0}' is an unknown PDO data source.", dsn));
} else if (_user != null && !"".equals(_user)) {
_conn = ds.getConnection(_user, _password);
} else {
_conn = ds.getConnection();
}
// TODO: move this code into a parseOptions() function
if (options != null) {
// gh#29
if (options.keyExists(ATTR_ERRMODE_V)) {
_error.setErrmode(options.get(ATTR_ERRMODE_V).toInt());
}
if (options.keyExists(MYSQL_ATTR_INIT_COMMAND_V)) {
exec(options.get(MYSQL_ATTR_INIT_COMMAND_V).toString());
}
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
env.warning(e.getMessage(), e);
_error.error(e);
}
}
/**
* Starts a transaction.
*/
public boolean beginTransaction() {
if (_conn == null) {
return false;
}
if (_inTransaction) {
return false;
}
_inTransaction = true;
try {
_conn.setAutoCommit(false);
return true;
} catch (SQLException e) {
_error.error(e);
return false;
}
}
private void closeStatements() {
Statement lastStatement = _lastStatement;
_lastInsertId = null;
_lastStatement = null;
_lastPDOStatement = null;
try {
if (lastStatement != null) {
lastStatement.close();
}
} catch (Throwable t) {
log.log(Level.WARNING, t.toString(), t);
}
}
/**
* Commits a transaction.
*/
public boolean commit() {
if (_conn == null) {
return false;
}
if (!_inTransaction) {
return false;
}
_inTransaction = false;
boolean result = false;
try {
_conn.commit();
_conn.setAutoCommit(true);
return true;
} catch (SQLException e) {
_error.error(e);
}
return result;
}
public void close() {
cleanup();
}
/**
* Implements the EnvCleanup interface.
*/
@Override
public void cleanup() {
Connection conn = _conn;
_conn = null;
closeStatements();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
log.log(Level.WARNING, e.toString(), e);
}
}
}
public String errorCode() {
return _error.errorCode();
}
public ArrayValue errorInfo() {
return _error.errorInfo();
}
/**
* Executes a statement, returning the number of rows.
*/
public int exec(String query)
throws SQLException {
if (_conn == null) {
return -1;
}
closeStatements();
Statement stmt = null;
int rowCount;
try {
stmt = _conn.createStatement();
stmt.setEscapeProcessing(false);
if (stmt.execute(query)) {
ResultSet resultSet = null;
try {
resultSet = stmt.getResultSet();
resultSet.last();
rowCount = resultSet.getRow();
_lastStatement = stmt;
stmt = null;
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
} catch (SQLException e) {
log.log(Level.FINER, e.toString(), e);
}
}
} else {
rowCount = stmt.getUpdateCount();
_lastStatement = stmt;
stmt = null;
}
} catch (SQLException e) {
_error.error(e);
return -1;
} finally {
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException e) {
log.log(Level.FINER, e.toString(), e);
}
}
return rowCount;
}
public Value getAttribute(int attribute) {
switch (attribute) {
case ATTR_AUTOCOMMIT:
return BooleanValue.create(getAutocommit());
case ATTR_CASE:
return LongValue.create(getCase());
case ATTR_CLIENT_VERSION:
throw new UnimplementedException();
case ATTR_CONNECTION_STATUS:
throw new UnimplementedException();
case ATTR_DRIVER_NAME:
throw new UnimplementedException();
case ATTR_ERRMODE:
return LongValue.create(_error.getErrmode());
case ATTR_ORACLE_NULLS:
return LongValue.create(getOracleNulls());
case ATTR_PERSISTENT:
return BooleanValue.create(getPersistent());
case ATTR_PREFETCH:
return LongValue.create(getPrefetch());
case ATTR_SERVER_INFO:
return StringValue.create(getServerInfo());
case ATTR_SERVER_VERSION:
return StringValue.create(getServerVersion());
case ATTR_TIMEOUT:
return LongValue.create(getTimeout());
default:
_error.unsupportedAttribute(attribute);
// TODO: check what php does
return BooleanValue.FALSE;
}
}
public static ArrayValue getAvailableDrivers() {
ArrayValue array = new ArrayValueImpl();
array.put("mysql");
array.put("pgsql");
array.put("java");
array.put("jdbc");
return array;
}
/**
* Returns the auto commit value for the connection.
*/
private boolean getAutocommit() {
if (_conn == null) {
return true;
}
try {
return _conn.getAutoCommit();
} catch (SQLException e) {
_error.error(e);
return true;
}
}
public int getCase() {
throw new UnimplementedException();
}
public int getOracleNulls() {
throw new UnimplementedException();
}
private boolean getPersistent() {
return true;
}
private int getPrefetch() {
throw new UnimplementedException();
}
private String getServerInfo() {
throw new UnimplementedException();
}
// TODO: might be int return
private String getServerVersion() {
throw new UnimplementedException();
}
private int getTimeout() {
throw new UnimplementedException();
}
public String lastInsertId(@Optional String name) {
if (!(name == null || name.length() == 0)) {
throw new UnimplementedException("lastInsertId with name");
}
if (_lastInsertId != null) {
return _lastInsertId;
}
String lastInsertId = null;
if (_lastPDOStatement != null) {
lastInsertId = _lastPDOStatement.lastInsertId(name);
} else if (_lastStatement != null) {
ResultSet resultSet = null;
try {
resultSet = _lastStatement.getGeneratedKeys();
if (resultSet.next()) {
lastInsertId = resultSet.getString(1);
}
} catch (SQLException ex) {
_error.error(ex);
} finally {
try {
if (resultSet != null) {
resultSet.close();
}
} catch (SQLException ex) {
log.log(Level.WARNING, ex.toString(), ex);
}
}
}
_lastInsertId = lastInsertId == null ? "0" : lastInsertId;
return _lastInsertId;
}
/**
* Prepares a statement for execution.
*/
@ReturnNullAsFalse
public PDOStatement prepare(String statement,
@Optional ArrayValue driverOptions) {
if (_conn == null) {
return null;
}
try {
closeStatements();
PDOStatement pdoStatement = new PDOStatement(_env, _conn, statement, true, driverOptions);
_lastPDOStatement = pdoStatement;
return pdoStatement;
} catch (SQLException e) {
_error.error(e);
return null;
}
}
/**
* Queries the database
*/
public Value query(String query) {
if (_conn == null) {
return BooleanValue.FALSE;
}
try {
closeStatements();
PDOStatement pdoStatement = new PDOStatement(
_env, _conn, query, false, null);
_lastPDOStatement = pdoStatement;
return _env.wrapJava(pdoStatement);
} catch (SQLException e) {
_error.error(e);
return BooleanValue.FALSE;
}
}
/**
* Quotes the string
*/
public String quote(String query, @Optional int parameterType) {
return "'" + real_escape_string(query) + "'";
}
/**
* Escapes the string.
*/
public String real_escape_string(String str) {
StringBuilder buf = new StringBuilder();
final int strLength = str.length();
for (int i = 0; i < strLength; i++) {
char c = str.charAt(i);
switch (c) {
case '\u0000':
buf.append('\\');
buf.append('\u0000');
break;
case '\n':
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '"':
buf.append('\\');
buf.append('\"');
break;
case '\032':
buf.append('\\');
buf.append('Z');
break;
default:
buf.append(c);
break;
}
}
return buf.toString();
}
/**
* Rolls a transaction back.
*/
public boolean rollBack() {
if (_conn == null) {
return false;
}
if (!_inTransaction) {
return false;
}
_inTransaction = false;
try {
_conn.rollback();
_conn.setAutoCommit(true);
return true;
} catch (SQLException e) {
_error.error(e);
return false;
}
}
public boolean setAttribute(int attribute, Value value) {
return setAttribute(attribute, value, false);
}
private boolean setAttribute(int attribute, Value value, boolean isInit) {
switch (attribute) {
case ATTR_AUTOCOMMIT:
return setAutocommit(value.toBoolean());
case ATTR_ERRMODE:
return _error.setErrmode(value.toInt());
case ATTR_CASE:
return setCase(value.toInt());
case ATTR_ORACLE_NULLS:
return setOracleNulls(value.toInt());
case ATTR_STRINGIFY_FETCHES:
return setStringifyFetches(value.toBoolean());
case ATTR_STATEMENT_CLASS:
return setStatementClass(value);
}
if (isInit) {
switch (attribute) {
// TODO: there may be more of these
case ATTR_TIMEOUT:
return setTimeout(value.toInt());
case ATTR_PERSISTENT:
return setPersistent(value.toBoolean());
}
}
// TODO: check what PHP does
_error.unsupportedAttribute(attribute);
return false;
}
/**
* Sets the auto commit, if true commit every statement.
* @return true on success, false on error.
*/
private boolean setAutocommit(boolean autoCommit) {
if (_conn == null) {
return false;
}
try {
_conn.setAutoCommit(autoCommit);
} catch (SQLException e) {
_error.error(e);
return false;
}
return true;
}
/**
* Force column names to a specific case.
*
* <dl>
* <dt>{@link CASE_LOWER}
* <dt>{@link CASE_NATURAL}
* <dt>{@link CASE_UPPER}
* </dl>
*/
private boolean setCase(int value) {
switch (value) {
case CASE_LOWER:
case CASE_NATURAL:
case CASE_UPPER:
throw new UnimplementedException();
default:
_error.unsupportedAttributeValue(value);
return false;
}
}
/**
* Sets whether or not the convert nulls and empty strings, works for
* all drivers.
*
* <dl>
* <dt> {@link NULL_NATURAL}
* <dd> no conversion
* <dt> {@link NULL_EMPTY_STRING}
* <dd> empty string is converted to NULL
* <dt> {@link NULL_TO_STRING} NULL
* <dd> is converted to an empty string.
* </dl>
*
* @return true on success, false on error.
*/
private boolean setOracleNulls(int value) {
switch (value) {
case NULL_NATURAL:
case NULL_EMPTY_STRING:
case NULL_TO_STRING:
throw new UnimplementedException();
default:
_error.warning(L.l("unknown value `{0}'", value));
_error.unsupportedAttributeValue(value);
return false;
}
}
private boolean setPersistent(boolean isPersistent) {
return true;
}
private boolean setPrefetch(int prefetch) {
throw new UnimplementedException();
}
/**
* Sets a custom statement class derived from PDOStatement.
*
* @param value an array(classname, array(constructor args)).
*
* @return true on success, false on error.
*/
private boolean setStatementClass(Value value) {
throw new UnimplementedException("ATTR_STATEMENT_CLASS");
}
/**
* Convert numeric values to strings when fetching.
*
* @return true on success, false on error.
*/
private boolean setStringifyFetches(boolean stringifyFetches) {
throw new UnimplementedException();
}
private boolean setTimeout(int timeoutSeconds) {
throw new UnimplementedException();
}
/**
* Opens a connection based on the dsn.
*/
private DataSource getDataSource(Env env, String dsn)
throws Exception {
if (dsn.startsWith("mysql:")) {
return getMysqlDataSource(env, dsn);
}
if (dsn.startsWith("pgsql:")) {
return getPgsqlDataSource(env, dsn);
} else if (dsn.startsWith("java")) {
return getJndiDataSource(env, dsn);
} else if (dsn.startsWith("jdbc:")) {
return getJdbcDataSource(env, dsn);
} else if (dsn.indexOf(":jdbc:") > 0) {
return getJdbcDataSource(env, dsn);
} else if (dsn.startsWith("resin:")) {
return getResinDataSource(env, dsn);
} else {
env.error(L.l("'{0}' is an unknown PDO data source.",
dsn));
return null;
}
}
/**
* Opens a jdbc connection based on the dsn.
* you can also use "driver-class:jdbc-url"
*/
private DataSource getJdbcDataSource(Env env, String dsn)
throws Exception {
if (dsn.indexOf(":jdbc:") > 0) {
return env.getDataSource(dsn.substring(0, dsn.indexOf(":jdbc:")), dsn.substring(dsn.indexOf(":jdbc:") + 1));
}
int t_start = dsn.indexOf(':') + 1;
int t_end = dsn.indexOf(':', t_start);
String tag = dsn.substring(t_start, t_end);
if (driverList.containsKey(tag)) {
return env.getDataSource(driverList.getProperty(tag), dsn);
}
return env.getDataSource(null, dsn);
}
/**
* Opens a mysql connection based on the dsn.
*/
private DataSource getMysqlDataSource(Env env, String dsn)
throws Exception {
HashMap<String, String> attr = parseAttr(dsn, dsn.indexOf(':'));
// TODO: more robust to get attribute values as is done in getPgsqlDataSource
String host = attr.get("host");
String portStr = attr.get("port");
String dbname = attr.get("dbname");
String unixSocket = attr.get("unix_socket");
if (unixSocket != null) {
env.error(L.l("PDO mysql: does not support unix_socket"));
return null;
}
if (host == null) {
host = "localhost";
}
int port = 3306;
if (portStr != null) {
try {
port = Integer.parseInt(portStr);
} catch (NumberFormatException e) {
log.log(Level.FINE, e.toString(), e);
}
}
if (dbname == null) {
dbname = "test";
}
// PHP doc does not sure user and password as attributes for mysql,
// but in the pgsql doc the dsn specified user and
// password override arguments
// passed to constructor
String user = attr.get("user");
if (user != null) {
_user = user;
}
String password = attr.get("password");
if (password != null) {
_password = password;
}
String driver = Mysqli.DRIVER;
// TODO: mysql options?
String url = Mysqli.getUrl(host, port, dbname, ENCODING,
false, false, false);
return env.getDataSource(driver, url);
}
/**
* Opens a postgres connection based on the dsn.
*/
private DataSource getPgsqlDataSource(Env env, String dsn)
throws Exception {
HashMap<String, String> attr = parseAttr(dsn, dsn.indexOf(':'));
String host = "localhost";
String port = null;
String dbname = "test";
String user = null;
String password = null;
for (Map.Entry<String, String> entry : attr.entrySet()) {
String key = entry.getKey();
if ("host".equals(key)) {
host = entry.getValue();
} else if ("port".equals(key)) {
port = entry.getValue();
} else if ("dbname".equals(key)) {
dbname = entry.getValue();
} else if ("user".equals(key)) {
user = entry.getValue();
} else if ("password".equals(key)) {
password = entry.getValue();
} else {
env.warning(L.l("unknown pgsql attribute '{0}'", key));
}
}
// confirmed with PHP doc, the dsn specified user and password override
// arguments passed to constructor
if (user != null) {
_user = user;
}
if (password != null) {
_password = password;
}
String driver = "org.postgresql.Driver";
StringBuilder url = new StringBuilder();
url.append("jdbc:postgresql://");
url.append(host);
if (port != null) {
url.append(':');
url.append(port);
}
url.append('/');
url.append(dbname);
return env.getDataSource(driver, url.toString());
}
/**
* Opens a resin connection based on the dsn.
*/
private DataSource getResinDataSource(Env env, String dsn)
throws Exception {
String driver = "com.caucho.db.jdbc.ConnectionPoolDataSourceImpl";
String url = "jdbc:" + dsn;
return env.getDataSource(driver, url);
}
/**
* Opens a connection based on the dsn.
*/
private DataSource getJndiDataSource(Env env, String dsn) {
DataSource ds = null;
try {
Context ic = new InitialContext();
ds = (DataSource) ic.lookup(dsn);
} catch (NamingException e) {
log.log(Level.FINE, e.toString(), e);
}
if (ds == null) {
env.error(L.l("'{0}' is an unknown PDO JNDI data source.", dsn));
}
return ds;
}
private HashMap<String, String> parseAttr(String dsn, int i) {
HashMap<String, String> attr = new LinkedHashMap<String, String>();
int length = dsn.length();
for (; i < length; i++) {
char ch = dsn.charAt(i);
if (!Character.isJavaIdentifierStart(ch)) {
continue;
}
StringBuilder name = new StringBuilder();
for (;
i < length && Character.isJavaIdentifierPart((ch = dsn.charAt(i)));
i++) {
name.append(ch);
}
for (; i < length && ((ch = dsn.charAt(i)) == ' ' || ch == '='); i++) {
}
StringBuilder value = new StringBuilder();
for (; i < length && (ch = dsn.charAt(i)) != ' ' && ch != ';'; i++) {
value.append(ch);
}
attr.put(name.toString(), value.toString());
}
return attr;
}
@Override
public String toString() {
// do not show password!
if (_dsn == null) {
return "PDO[]";
}
if (_dsn.indexOf("pass") < 0) {
return "PDO[" + _dsn + "]";
}
int i = _dsn.lastIndexOf(':');
if (i < 0) {
return "PDO[]";
}
if (_dsn.startsWith("java")) {
return "PDO[" + _dsn + "]";
}
StringBuilder str = new StringBuilder();
str.append("PDO[");
str.append(_dsn, 0, i + 1);
HashMap<String, String> attr = parseAttr(_dsn, i);
if (_user != null && !"".equals(_user)) {
attr.put("user", _user);
}
boolean first = true;
for (Map.Entry<String, String> entry : attr.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if ("password".equalsIgnoreCase(key)) {
value = "XXXXX";
} else if ("passwd".equalsIgnoreCase(key)) {
value = "XXXXX";
} else if ("pass".equalsIgnoreCase(key)) {
value = "XXXXX";
}
if (!first) {
str.append(' ');
}
first = false;
str.append(key);
str.append("=");
str.append(value);
}
str.append("]");
return str.toString();
}
}