serge       01/12/05 14:10:14

  Added:       src/java/org/apache/james/util/mordred JdbcDataSource.java
                        PoolConnEntry.java
  Log:
  Excalibur and Town create Mordred, a reliable database connection pooler in an 
Avalon architecture.
  
  Revision  Changes    Path
  1.1                  
jakarta-james/src/java/org/apache/james/util/mordred/JdbcDataSource.java
  
  Index: JdbcDataSource.java
  ===================================================================
  /*
   * Copyright (C) The Apache Software Foundation. All rights reserved.
   *
   * This software is published under the terms of the Apache Software License
   * version 1.1, a copy of which has been included with this distribution in
   * the LICENSE file.
   */
  package org.apache.james.util.mordred;
  
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.sql.Connection;
  import java.sql.SQLException;
  import java.util.Vector;
  import org.apache.avalon.framework.activity.Disposable;
  import org.apache.avalon.framework.configuration.Configurable;
  import org.apache.avalon.framework.configuration.Configuration;
  import org.apache.avalon.framework.configuration.ConfigurationException;
  import org.apache.avalon.framework.logger.AbstractLoggable;
  
  //Notice how cornerstone is dependent on Excalibur
  import org.apache.avalon.excalibur.datasource.DataSourceComponent;
  
  /**
   * <p>This is a <b>reliable</b> DataSource implementation, based on the pooling
   * logic written for <a href="http://www.whichever.com/";>Town</a> and the
   * configuration found in Avalon's excalibur code.
   * </p>
   * <p>This uses the normal <code>java.sql.Connection</code> object and
   * <code>java.sql.DriverManager</code>.  The Configuration is like this:
   *
   * <pre>
   *   &lt;jdbc&gt;
   *     &lt;pool-controller min="<i>5</i>" max="<i>10</i>" 
connection-class="<i>my.overrided.ConnectionClass</i>"&gt;
   *       &lt;keep-alive&gt;select 1&lt;/keep-alive&gt;
   *     &lt;/pool-controller&gt;
   *     &lt;driver&gt;<i>com.database.jdbc.JdbcDriver</i>&lt;/driver&gt;
   *     &lt;dburl&gt;<i>jdbc:driver://host/mydb</i>&lt;/dburl&gt;
   *     &lt;user&gt;<i>username</i>&lt;/user&gt;
   *     &lt;password&gt;<i>password</i>&lt;/password&gt;
   *   &lt;/jdbc&gt;
   * </pre>
   *
   * @author <a href="mailto:[EMAIL PROTECTED]";>Serge Knystautas</a>
   * @version CVS $Revision: 1.1 $ $Date: 2001/12/05 22:10:14 $
   * @since 4.0
   */
  public class JdbcDataSource
      extends AbstractLoggable implements Configurable, Runnable, Disposable, 
DataSourceComponent
  {
      /**
       * Configure and set up DB connection.  Here we set the connection
       * information needed to create the Connection objects.
       *
       * @param conf The Configuration object needed to describe the
       *             connection.
       *
       * @throws ConfigurationException
       */
      public void configure( final Configuration configuration )
          throws ConfigurationException
      {
          try {
              jdbcDriver = configuration.getChild("driver").getValue(null);
              jdbcURL = configuration.getChild("dburl").getValue(null);
              jdbcUsername = configuration.getChild("user").getValue(null);
              jdbcPassword = configuration.getChild("password").getValue(null);
  
              maxConn = configuration.getChild("max").getValueAsInteger(2);
              //logfilename?
              verifyConnSQL = configuration.getChild("keep-alive").getValue(null);
  
              //Not support from Town: logfilename
  
              //Not supporting from Excalibur: pool-controller, min, auto-commit, 
oradb, connection-class
  
              if (jdbcDriver == null) {
                  throw new ConfigurationException("You need to specify a valid 
driver, e.g., <driver>my.class</driver>");
              }
  
              try {
                  getLogger().debug("Loading new driver: " + jdbcDriver);
                  Class.forName(jdbcDriver, true, 
Thread.currentThread().getContextClassLoader());
              } catch (ClassNotFoundException cnfe) {
                  throw new ConfigurationException("'" + jdbcDriver + "' could not be 
found in classloader.  Please specify a valid JDBC driver");
              }
  
              if (jdbcURL == null) {
                  throw new ConfigurationException("You need to specify a valid JDBC 
connection string, e.g., <dburl>jdbc:driver:database</dburl>");
              }
  
              if (maxConn < 1) {
                  throw new ConfigurationException("Maximum number of connections 
specified must be at least 1.");
              }
  
              getLogger().debug("Starting connection pooler");
              getLogger().debug("driver = " + jdbcDriver);
              getLogger().debug("dburl = " + jdbcURL);
              getLogger().debug("username = " + jdbcUsername);
              //We don't show the password
              getLogger().debug("max connections = " + maxConn);
  
              pool = new Vector();
              reaperActive = true;
              reaper = new Thread(this);
  
              reaper.setDaemon(true);
              reaper.start();
  
          } catch (ConfigurationException ce) {
              //Let this pass through...
              throw ce;
          } catch (Exception e) {
              throw new ConfigurationException("Error configuring JdbcDataSource", e);
          }
      }
  
  
  
      private static int total_served = 0;
  
      // The limit that an active connection can be running
      public final static long ACTIVE_CONN_TIME_LIMIT = 60000; // (one minute)
  
      // How long before you kill off a connection due to inactivity
      public final static long CONN_IDLE_LIMIT = 600000; // (10 minutes)
  
      // Thread that checks for dead/aged connections and removes them from pool
      private Thread      reaper;
  
      // Flag to indicate whether reaper thread should run
      private boolean reaperActive = false;
  
      // Driver class
      private String      jdbcDriver;
  
      // Server to connect to database (this really is the jdbc URL)
      private String      jdbcURL;
  
      // Username to login to database
      private String      jdbcUsername;
  
      // Password to login to database
      private String      jdbcPassword;
  
      // Maximum number of connections to have open at any point
      private int         maxConn = -1;
  
      // collection of connection objects
      private Vector      pool;
  
      // connection number for like of this broker
      private int         connectionCount;
  
      // This is a temporary variable used to track how many active threads
      // are in createConnection().  This is to prevent to many connections
      // from being opened at once.
      private int         connCreationsInProgress = 0;
  
      // the last time a connection was created...
      private long        connLastCreated = 0;
  
      // a SQL command to execute to see if the connection is still ok
      private String      verifyConnSQL;
  
      // The error message is the conn pooler cannot serve connections for whatever 
reason
      private String      connErrorMessage = null;
  
  
  
  
  
  
      /**
       * Creates a new connection as per these properties, adds it to the pool,
       * and logs the creation.
       *
  
       * @return PoolConnEntry the new connection wrapped as an entry
       *
  
       * @throws SQLException
       */
      private PoolConnEntry createConn() throws SQLException {
          PoolConnEntry entry = null;
  
          synchronized (this) {
              if (connCreationsInProgress > 0) {
                  //We are already creating one in another place
                  return null;
              }
  
              long now = System.currentTimeMillis();
              if (now - connLastCreated < 1000 * pool.size()) {
                  //We don't want to scale up too quickly...
                  return null;
              }
  
              if (maxConn == -1 || pool.size() < maxConn) {
                  connCreationsInProgress++;
                  connLastCreated = now;
              } else {
                  // We've already hit a limit... fail silently
                  getLogger().debug("Connection limit hit... " + pool.size() + " in 
pool and " + connCreationsInProgress + " + on the way.");
                  return null;
              }
          }
  
          try {
              entry = new PoolConnEntry(this,
                      java.sql.DriverManager.getConnection(jdbcURL, jdbcUsername, 
jdbcPassword),
                      ++connectionCount);
              getLogger().debug("Opening connection " + entry);
              entry.lock();
              pool.addElement(entry);
              return entry;
          } catch (SQLException sqle) {
              //Shouldn't ever happen, but it did, just return null.
              return null;
          } finally {
              synchronized (this) {
                  connCreationsInProgress--;
              }
  
          }
      }
  
      /**
       * Implements the ConnDefinition behavior when a connection is no longer needed.
       * This resets flags on the wrapper of the connection to allow others to use
       * this connection.
       *
  
       * @param java.sql.Connection
       */
      public void releaseConnection(PoolConnEntry entry) {
          //PoolConnEntry entry = findEntry(conn);
  
          if (entry != null) {
              entry.unlock();
              return;
          } else {
              getLogger().warn("----> Could not find the connection to free!!!");
              return;
          }
      }
  
      /**
       * Implements the ConnDefinition behavior when something bad has happened to
       * a connection.
       * If a sql command was provided in the properties file, it will run this and
       * attempt to determine whether the connection is still valid.  If it is,
       * it recycles this connection back into the pool.  If it is not, it closes
       * the connection.
       *
       * @param java.sql.Connection the connection that had problems
       *
       * @deprecated - No longer used in the new approach.
       */
      public void killConnection(PoolConnEntry entry) {
          if (entry != null) {
              // if we were provided SQL to test the connection with, we will use
              // this and possibly just release the connection after clearing warnings
              if (verifyConnSQL != null) {
                  try {
                      // Test this connection
                      java.sql.Statement stmt = entry.createStatement();
                      stmt.execute(verifyConnSQL);
                      stmt.close();
                      // Passed test... recycle the entry
                      entry.unlock();
                  } catch (SQLException e1) {
                      // Failed test... close the entry
                      finalizeEntry(entry);
                  }
              } else {
                  // No SQL was provided... we have to kill this entry to be sure
                  finalizeEntry(entry);
              }
              return;
          } else {
              getLogger().warn("----> Could not find connection to kill!!!");
              new Throwable().printStackTrace();
              return;
          }
      }
  
      /**
       * Closes a connection and removes it from the pool.
       * @param PoolConnEntry entry
       */
      private synchronized void finalizeEntry(PoolConnEntry entry) {
          pool.removeElement(entry);
      }
  
      /**
       * Implements the ConnDefinition behavior when a connection is needed.
       * Checks the pool of connections to see if there is one available.  If there
       * is not and we are below the max number of connections limit, it tries to 
create
       * another connection.  It retries this 10 times until a connection is available 
or
       * can be created
       *
       * @return java.sql.Connection
       */
      public Connection getConnection() throws SQLException {
          //If the conn definition has a fatal connection problem, need to return that 
error
          if (connErrorMessage != null) {
              throw new SQLException(connErrorMessage);
          }
  
          //Look through our list of open connections right now, starting from 
beginning.
  
          //If we find one, book it.
  
          int count = total_served++;
          //System.out.println(new java.util.Date() + " trying to get a connection (" 
+ count + ")");
          for (int attempts = 1; attempts <= 100; attempts++) {
              synchronized (pool) {
                  for (int i = 0; i < pool.size(); i++) {
                      PoolConnEntry entry = (PoolConnEntry)pool.elementAt(i);
                      //Set the appropriate flags to make this connection
                      //marked as in use
                      try {
                          if (entry.lock()) {
                              //System.out.println(new java.util.Date() + " return a 
connection (" + count + ")");
                              return entry;
                          }
                      } catch (SQLException se) {
                          //Somehow a closed connection appeared in our pool.
                          //Remove it immediately.
                          finalizeEntry(entry);
                          continue;
                      }
                      //we were unable to get a lock on this entry.. so continue 
through list
                  } //loop through existing connections
              }
  
              //If we have 0, create another
              try {
                  if (pool.size() == 0) {
                      //create a connection
                      PoolConnEntry entry = createConn();
                      if (entry != null) {
                          //System.out.println(new java.util.Date() + " returning new 
connection (" + count + ")");
                          return entry;
                      }
                      //looks like a connection was already created
                  } else {
  
                      //Since we didn't find one, and we have < max connections, then 
consider whether
                      //  we create another
  
                      //if we've hit the 3rd attempt without getting a connection,
                      //  let's create another to anticipate a slow down
                      if (attempts == 20 && (pool.size() < maxConn || maxConn == -1)) {
                          PoolConnEntry entry = createConn();
                          if (entry != null) {
                              //System.out.println(" returning new connection (" + 
count + "(");
                              return entry;
                          } else {
                              attempts = 1;
                          }
                      }
                  }
              } catch (SQLException sqle) {
                  //Ignore... error creating the connection
                  StringWriter sout = new StringWriter();
                  PrintWriter pout = new PrintWriter(sout, true);
                  pout.println("Error creating connection: ");
                  sqle.printStackTrace(pout);
                  getLogger().error(sout.toString());
              }
  
              //otherwise sleep 20ms 10 times, then create a connection
              try {
                  Thread.currentThread().sleep(50);
              } catch (InterruptedException ie) {
              }
          }
  
          // Give up... no connections available
          throw new SQLException("Giving up... no connections available.");
      }
  
      /**
       * Background thread that checks if there are fewer connections open than
       * minConn specifies, and checks whether connections have been checked out
       * for too long, killing them.
       */
      public void run() {
          while (reaperActive) {
              for (int i = 0; i < pool.size(); i++) {
                  PoolConnEntry entry = (PoolConnEntry) pool.elementAt(i);
  
                  long age = System.currentTimeMillis()
                             - entry.getLastActivity();
  
                  synchronized (entry) {
                      if (entry.getStatus() == PoolConnEntry.ACTIVE &&
                              age > ACTIVE_CONN_TIME_LIMIT) {
                          getLogger().info(" ***** connection " + entry.getId() + " is 
way too old: "
                              + age + " > " + ACTIVE_CONN_TIME_LIMIT);
  
                          // This connection is way too old...
                          // kill it no matter what
                          finalizeEntry(entry);
                          continue;
                      }
  
                      if (entry.getStatus() == PoolConnEntry.AVAILABLE &&
                              age > CONN_IDLE_LIMIT) {
                          //We've got a connection that's too old... kill it
                          finalizeEntry(entry);
                          continue;
                      }
                  }
              }
              try {
  
                  // Check for activity every 5 seconds
                  Thread.sleep(5000L);
              } catch (InterruptedException ex) {}
          }
      }
  
      /**
       * Close all connections.  The connection pooler will recreate these connections
       * if something starts requesting them again.
       *
       * @deprecated This was left over code from Town... but not exposed in Avalon.
       */
      public synchronized void killAllConnections() {
          //Just remove the references to all the connections... this will cause them 
to get
          // finalized before very long. (not an instant shutdown, but that's ok).
          pool.clear();
      }
  
      /**
       * Need to clean up all connections
       */
      public void dispose() {
  
          // Stop the background monitoring thread
          if (reaper != null) {
              reaperActive = false;
              //In case it's sleeping, help it quit faster
              reaper.interrupt();
              reaper = null;
          }
  
          // The various entries will finalize themselves once the reference
          // is removed, so no need to do it here
      }
  
      /*
       * This is a real hack, but oh well for now
       */
      protected void warn(String message) { getLogger().warn(message); }
      protected void info(String message) { getLogger().info(message); }
      protected void debug(String message) { getLogger().debug(message); }
  
  }
  
  
  
  1.1                  
jakarta-james/src/java/org/apache/james/util/mordred/PoolConnEntry.java
  
  Index: PoolConnEntry.java
  ===================================================================
  /*
   * Copyright (C) The Apache Software Foundation. All rights reserved.
   *
   * This software is published under the terms of the Apache Software License
   * version 1.1, a copy of which has been included with this distribution in
   * the LICENSE file.
   */
  package org.apache.james.util.mordred;
  
  import java.io.*;
  import java.sql.*;
  import java.util.*;
  
  /**
   * Insert the type's description here.
   * Creation date: (8/24/99 11:41:10 AM)
   * @author: Serge Knystautas <[EMAIL PROTECTED]>
   */
  public class PoolConnEntry implements java.sql.Connection{
  
      // States for connections (in use, being tested, or active)
      public final static int     AVAILABLE = 0;
      public final static int     ACTIVE = 1;
      private JdbcDataSource      container;
      private Connection          connection;
      private int                 status;
      private long                lockTime;
      private long                createDate;
      private long                lastActivity;
      private int                 id;
      private java.lang.Throwable trace;
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 11:43:45 AM)
       * @param conn java.sql.Connection
       */
      public PoolConnEntry(JdbcDataSource container, Connection conn, int id) {
          this.container = container;
          this.connection = conn;
          status = AVAILABLE;
          createDate = System.currentTimeMillis();
          lastActivity = System.currentTimeMillis();
          this.id = id;
      }
  
      /**
       * Locks an entry for anybody else using it
       */
      public synchronized boolean lock() throws SQLException {
          if (status != PoolConnEntry.AVAILABLE) {
              return false;
          }
  
          if (connection.isClosed()) {
              throw new SQLException("Connection has been closed.");
          }
  
          status = PoolConnEntry.ACTIVE;
          lockTime = System.currentTimeMillis();
          lastActivity = lockTime;
          trace = new Throwable();
          clearWarnings();
          return true;
      }
  
      /**
       * Resets flags on an entry for reuse in the pool
       */
      public synchronized void unlock() {
          lastActivity = System.currentTimeMillis();
          trace = null;
          status = AVAILABLE;
      }
  
      /**
       * Simple method to log any warnings on an entry (connection), and
       * then clear them.
       * @throws java.sql.SQLException
       */
      public void clearWarnings() {
          try {
              SQLWarning currSQLWarning = connection.getWarnings();
              while (currSQLWarning != null) {
                  container.debug("Warnings on connection " + id + " " + 
currSQLWarning);
                  currSQLWarning = currSQLWarning.getNextWarning();
              }
              connection.clearWarnings();
          } catch (SQLException sqle) {
              container.debug("Error while clearing exceptions on " + id);
              // It will probably get killed by itself before too long if this failed
          }
      }
  
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 11:43:19 AM)
       * @return long
       */
      public long getCreateDate() {
          return createDate;
      }
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 12:09:01 PM)
       * @return int
       */
      public int getId() {
          return id;
      }
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 11:43:19 AM)
       * @return long
       */
      public long getLastActivity() {
          return lastActivity;
      }
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 11:43:19 AM)
       * @return long
       */
      public long getLockTime() {
          return lockTime;
      }
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 11:43:19 AM)
       * @return int
       */
      public int getStatus() {
          return status;
      }
  
      /**
       * Insert the method's description here.
       * Creation date: (8/24/99 2:33:38 PM)
       * @return java.lang.Throwable
       */
      public java.lang.Throwable getTrace() {
          return trace;
      }
  
      /**
       * Need to clean up the connection
       */
      protected void finalize() {
          container.debug("Closing connection " + id);
          try {
              connection.close();
          } catch (SQLException ex) {
              container.warn("Cannot close connection " + id + " on finalize");
          }
          // Dump the stack trace of whoever created this connection
          if (getTrace() != null) {
              StringWriter sout = new StringWriter();
              trace.printStackTrace(new PrintWriter(sout, true));
              container.info(sout.toString());
          }
      }
  
      public String getString() {
          return getId() + ": " + connection.toString();
      }
  
  
      /*
       * New approach now actually has this implement a connection, as a wrapper.
       * All calls will be passed to underlying connection.
       * Except when closed is called, which will instead cause the releaseConnection 
on
       * the parent to be executed.
       *
       * These are the methods from java.sql.Connection
       */
  
      public void close() throws SQLException {
          clearWarnings();
          container.releaseConnection(this);
      }
  
      public boolean isClosed() throws SQLException {
          return connection.isClosed();
      }
  
  
      public final Statement createStatement() throws SQLException {
          return connection.createStatement();
      }
  
      public final PreparedStatement prepareStatement(final String sql) throws 
SQLException {
          return connection.prepareStatement(sql);
      }
  
      public final CallableStatement prepareCall(final String sql) throws SQLException 
{
          return connection.prepareCall(sql);
      }
  
      public final String nativeSQL(final String sql) throws SQLException {
          return connection.nativeSQL( sql );
      }
  
      public final void setAutoCommit(final boolean autoCommit) throws SQLException {
          connection.setAutoCommit( autoCommit );
      }
  
      public final boolean getAutoCommit() throws SQLException {
          return connection.getAutoCommit();
      }
  
      public final void commit() throws SQLException {
          connection.commit();
      }
  
      public final void rollback() throws SQLException {
          connection.rollback();
      }
  
      public final DatabaseMetaData getMetaData() throws SQLException {
          return connection.getMetaData();
      }
  
      public final void setReadOnly( final boolean readOnly ) throws SQLException {
          connection.setReadOnly( readOnly );
      }
  
      public final boolean isReadOnly() throws SQLException {
          return connection.isReadOnly();
      }
  
      public final void setCatalog( final String catalog ) throws SQLException {
          connection.setCatalog( catalog );
      }
  
      public final String getCatalog() throws SQLException {
          return connection.getCatalog();
      }
  
      public final void setTransactionIsolation( final int level ) throws SQLException 
{
          connection.setTransactionIsolation(level);
      }
  
      public final int getTransactionIsolation() throws SQLException {
          return connection.getTransactionIsolation();
      }
  
      public final SQLWarning getWarnings() throws SQLException {
          return connection.getWarnings();
      }
  
      public final Statement createStatement( final int resultSetType,
                                              final int resultSetConcurrency )
              throws SQLException {
          return connection.createStatement(resultSetType, resultSetConcurrency);
      }
  
      public final PreparedStatement prepareStatement( final String sql,
                                                 final int resultSetType,
                                                 final int resultSetConcurrency )
              throws SQLException {
          return connection.prepareStatement( sql, resultSetType, 
resultSetConcurrency);
      }
  
      public final CallableStatement prepareCall( final String sql,
                                            final int resultSetType,
                                            final int resultSetConcurrency )
              throws SQLException {
          return connection.prepareCall( sql, resultSetType, resultSetConcurrency );
      }
  
      public final Map getTypeMap() throws SQLException {
          return connection.getTypeMap();
      }
  
      public final void setTypeMap( final Map map ) throws SQLException {
          connection.setTypeMap( map );
      }
  
      /*
  @JDBC3_START@
      */
  /*
      public final void setHoldability(int holdability)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final int getHoldability()
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final java.sql.Savepoint setSavepoint()
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final java.sql.Savepoint setSavepoint(String savepoint)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final void rollback(java.sql.Savepoint savepoint)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final void releaseSavepoint(java.sql.Savepoint savepoint)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final Statement createStatement(int resulSetType,
                                             int resultSetConcurrency,
                                             int resultSetHoldability)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final PreparedStatement prepareStatement(String sql,
                                          int resulSetType,
                                          int resultSetConcurrency,
                                          int resultSetHoldability)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final CallableStatement prepareCall(String sql,
                                          int resulSetType,
                                          int resultSetConcurrency,
                                          int resultSetHoldability)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final PreparedStatement prepareStatement(String sql,
                                          int autoGeneratedKeys)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final PreparedStatement prepareStatement(String sql,
                                          int[] columnIndexes)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  
      public final PreparedStatement prepareStatement(String sql,
                                          String[] columnNames)
          throws SQLException
      {
          throw new SQLException("This is not a Jdbc 3.0 Compliant Connection");
      }
  */
      /*
  @JDBC3_END@
      */
  
  
  }
  
  
  

--
To unsubscribe, e-mail:   <mailto:[EMAIL PROTECTED]>
For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>

Reply via email to