001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.dbcp2.cpdsadapter;
019
020import java.io.PrintWriter;
021import java.io.Serializable;
022import java.sql.DriverManager;
023import java.sql.SQLException;
024import java.sql.SQLFeatureNotSupportedException;
025import java.util.Hashtable;
026import java.util.Properties;
027import java.util.logging.Logger;
028
029import javax.naming.Context;
030import javax.naming.Name;
031import javax.naming.NamingException;
032import javax.naming.RefAddr;
033import javax.naming.Reference;
034import javax.naming.Referenceable;
035import javax.naming.StringRefAddr;
036import javax.naming.spi.ObjectFactory;
037import javax.sql.ConnectionPoolDataSource;
038import javax.sql.PooledConnection;
039
040import org.apache.commons.dbcp2.PoolablePreparedStatement;
041import org.apache.commons.pool2.KeyedObjectPool;
042import org.apache.commons.pool2.impl.BaseObjectPoolConfig;
043import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
044import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
045
046/**
047 * <p>
048 * An adapter for jdbc drivers that do not include an implementation
049 * of {@link javax.sql.ConnectionPoolDataSource}, but still include a
050 * {@link java.sql.DriverManager} implementation.
051 * <code>ConnectionPoolDataSource</code>s are not used within general
052 * applications.  They are used by <code>DataSource</code> implementations
053 * that pool <code>Connection</code>s, such as
054 * {@link org.apache.commons.dbcp2.datasources.SharedPoolDataSource}.  A J2EE
055 * container will normally provide some method of initializing the
056 * <code>ConnectionPoolDataSource</code> whose attributes are presented
057 * as bean getters/setters and then deploying it via JNDI.  It is then
058 * available as a source of physical connections to the database, when
059 * the pooling <code>DataSource</code> needs to create a new
060 * physical connection.
061 * </p>
062 *
063 * <p>
064 * Although normally used within a JNDI environment, the DriverAdapterCPDS
065 * can be instantiated and initialized as any bean and then attached
066 * directly to a pooling <code>DataSource</code>.
067 * <code>Jdbc2PoolDataSource</code> can use the
068 * <code>ConnectionPoolDataSource</code> with or without the use of JNDI.
069 * </p>
070 *
071 * <p>
072 * The DriverAdapterCPDS also provides <code>PreparedStatement</code> pooling
073 * which is not generally available in jbdc2
074 * <code>ConnectionPoolDataSource</code> implementation, but is
075 * addressed within the jdbc3 specification.  The <code>PreparedStatement</code>
076 * pool in DriverAdapterCPDS has been in the dbcp package for some time, but
077 * it has not undergone extensive testing in the configuration used here.
078 * It should be considered experimental and can be toggled with the
079 * poolPreparedStatements attribute.
080 * </p>
081 *
082 * <p>
083 * The <a href="package-summary.html">package documentation</a> contains an
084 * example using catalina and JNDI.  The <a
085 * href="../datasources/package-summary.html">datasources package documentation</a>
086 * shows how to use <code>DriverAdapterCPDS</code> as a source for
087 * <code>Jdbc2PoolDataSource</code> without the use of JNDI.
088 * </p>
089 *
090 * @author John D. McNally
091 * @version $Revision: 1572242 $ $Date: 2014-02-26 20:34:39 +0000 (Wed, 26 Feb 2014) $
092 * @since 2.0
093 */
094public class DriverAdapterCPDS
095    implements ConnectionPoolDataSource, Referenceable, Serializable,
096               ObjectFactory {
097
098    private static final long serialVersionUID = -4820523787212147844L;
099
100
101    private static final String GET_CONNECTION_CALLED
102            = "A PooledConnection was already requested from this source, "
103            + "further initialization is not allowed.";
104
105    /** Description */
106    private String description;
107    /** Password */
108    private String password;
109    /** Url name */
110    private String url;
111    /** User name */
112    private String user;
113    /** Driver class name */
114    private String driver;
115
116    /** Login TimeOut in seconds */
117    private int loginTimeout;
118    /** Log stream. NOT USED */
119    private transient PrintWriter logWriter = null;
120
121    // PreparedStatement pool properties
122    private boolean poolPreparedStatements;
123    private int maxIdle = 10;
124    private long _timeBetweenEvictionRunsMillis =
125            BaseObjectPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
126    private int _numTestsPerEvictionRun = -1;
127    private int _minEvictableIdleTimeMillis = -1;
128    private int _maxPreparedStatements = -1;
129
130    /** Whether or not getConnection has been called */
131    private volatile boolean getConnectionCalled = false;
132
133    /** Connection properties passed to JDBC Driver */
134    private Properties connectionProperties = null;
135
136    static {
137        // Attempt to prevent deadlocks - see DBCP - 272
138        DriverManager.getDrivers();
139    }
140
141    /**
142     * Controls access to the underlying connection
143     */
144    private boolean accessToUnderlyingConnectionAllowed = false;
145
146    /**
147     * Default no-arg constructor for Serialization
148     */
149    public DriverAdapterCPDS() {
150    }
151
152    /**
153     * Attempt to establish a database connection using the default
154     * user and password.
155     */
156    @Override
157    public PooledConnection getPooledConnection() throws SQLException {
158        return getPooledConnection(getUser(), getPassword());
159    }
160
161    /**
162     * Attempt to establish a database connection.
163     * @param username name to be used for the connection
164     * @param pass password to be used fur the connection
165     */
166    @Override
167    public PooledConnection getPooledConnection(String username, String pass)
168            throws SQLException {
169        getConnectionCalled = true;
170        PooledConnectionImpl pci = null;
171        // Workaround for buggy WebLogic 5.1 classloader - ignore the
172        // exception upon first invocation.
173        try {
174            if (connectionProperties != null) {
175                connectionProperties.put("user", username);
176                connectionProperties.put("password", pass);
177                pci = new PooledConnectionImpl(DriverManager.getConnection(
178                        getUrl(), connectionProperties));
179            } else {
180                pci = new PooledConnectionImpl(DriverManager.getConnection(
181                        getUrl(), username, pass));
182            }
183            pci.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed());
184        }
185        catch (ClassCircularityError e)
186        {
187            if (connectionProperties != null) {
188                pci = new PooledConnectionImpl(DriverManager.getConnection(
189                        getUrl(), connectionProperties));
190            } else {
191                pci = new PooledConnectionImpl(DriverManager.getConnection(
192                        getUrl(), username, pass));
193            }
194            pci.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed());
195        }
196        KeyedObjectPool<PStmtKeyCPDS, PoolablePreparedStatement<PStmtKeyCPDS>> stmtPool = null;
197        if (isPoolPreparedStatements()) {
198            GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig();
199            config.setMaxTotalPerKey(Integer.MAX_VALUE);
200            config.setBlockWhenExhausted(false);
201            config.setMaxWaitMillis(0);
202            config.setMaxIdlePerKey(getMaxIdle());
203            if (getMaxPreparedStatements() <= 0)
204            {
205                // since there is no limit, create a prepared statement pool with an eviction thread
206                //  evictor settings are the same as the connection pool settings.
207                config.setTimeBetweenEvictionRunsMillis(getTimeBetweenEvictionRunsMillis());
208                config.setNumTestsPerEvictionRun(getNumTestsPerEvictionRun());
209                config.setMinEvictableIdleTimeMillis(getMinEvictableIdleTimeMillis());
210            }
211            else
212            {
213                // since there is limit, create a prepared statement pool without an eviction thread
214                //  pool has LRU functionality so when the limit is reached, 15% of the pool is cleared.
215                // see org.apache.commons.pool2.impl.GenericKeyedObjectPool.clearOldest method
216                config.setMaxTotal(getMaxPreparedStatements());
217                config.setTimeBetweenEvictionRunsMillis(-1);
218                config.setNumTestsPerEvictionRun(0);
219                config.setMinEvictableIdleTimeMillis(0);
220            }
221            stmtPool = new GenericKeyedObjectPool<>(pci, config);
222            pci.setStatementPool(stmtPool);
223        }
224        return pci;
225    }
226
227    @Override
228    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
229        throw new SQLFeatureNotSupportedException();
230    }
231
232    // ----------------------------------------------------------------------
233    // Referenceable implementation
234
235    /**
236     * <CODE>Referenceable</CODE> implementation.
237     */
238    @Override
239    public Reference getReference() throws NamingException {
240        // this class implements its own factory
241        String factory = getClass().getName();
242
243        Reference ref = new Reference(getClass().getName(), factory, null);
244
245        ref.add(new StringRefAddr("description", getDescription()));
246        ref.add(new StringRefAddr("driver", getDriver()));
247        ref.add(new StringRefAddr("loginTimeout",
248                                  String.valueOf(getLoginTimeout())));
249        ref.add(new StringRefAddr("password", getPassword()));
250        ref.add(new StringRefAddr("user", getUser()));
251        ref.add(new StringRefAddr("url", getUrl()));
252
253        ref.add(new StringRefAddr("poolPreparedStatements",
254                                  String.valueOf(isPoolPreparedStatements())));
255        ref.add(new StringRefAddr("maxIdle",
256                                  String.valueOf(getMaxIdle())));
257        ref.add(new StringRefAddr("timeBetweenEvictionRunsMillis",
258            String.valueOf(getTimeBetweenEvictionRunsMillis())));
259        ref.add(new StringRefAddr("numTestsPerEvictionRun",
260            String.valueOf(getNumTestsPerEvictionRun())));
261        ref.add(new StringRefAddr("minEvictableIdleTimeMillis",
262            String.valueOf(getMinEvictableIdleTimeMillis())));
263        ref.add(new StringRefAddr("maxPreparedStatements",
264            String.valueOf(getMaxPreparedStatements())));
265
266        return ref;
267    }
268
269
270    // ----------------------------------------------------------------------
271    // ObjectFactory implementation
272
273    /**
274     * implements ObjectFactory to create an instance of this class
275     */
276    @Override
277    public Object getObjectInstance(Object refObj, Name name,
278                                    Context context, Hashtable<?,?> env)
279            throws Exception {
280        // The spec says to return null if we can't create an instance
281        // of the reference
282        DriverAdapterCPDS cpds = null;
283        if (refObj instanceof Reference) {
284            Reference ref = (Reference)refObj;
285            if (ref.getClassName().equals(getClass().getName())) {
286                RefAddr ra = ref.get("description");
287                if (ra != null && ra.getContent() != null) {
288                    setDescription(ra.getContent().toString());
289                }
290
291                ra = ref.get("driver");
292                if (ra != null && ra.getContent() != null) {
293                    setDriver(ra.getContent().toString());
294                }
295                ra = ref.get("url");
296                if (ra != null && ra.getContent() != null) {
297                    setUrl(ra.getContent().toString());
298                }
299                ra = ref.get("user");
300                if (ra != null && ra.getContent() != null) {
301                    setUser(ra.getContent().toString());
302                }
303                ra = ref.get("password");
304                if (ra != null && ra.getContent() != null) {
305                    setPassword(ra.getContent().toString());
306                }
307
308                ra = ref.get("poolPreparedStatements");
309                if (ra != null && ra.getContent() != null) {
310                    setPoolPreparedStatements(Boolean.valueOf(
311                        ra.getContent().toString()).booleanValue());
312                }
313                ra = ref.get("maxIdle");
314                if (ra != null && ra.getContent() != null) {
315                    setMaxIdle(Integer.parseInt(ra.getContent().toString()));
316                }
317
318                ra = ref.get("timeBetweenEvictionRunsMillis");
319                if (ra != null && ra.getContent() != null) {
320                    setTimeBetweenEvictionRunsMillis(
321                        Integer.parseInt(ra.getContent().toString()));
322                }
323
324                ra = ref.get("numTestsPerEvictionRun");
325                if (ra != null && ra.getContent() != null) {
326                    setNumTestsPerEvictionRun(
327                        Integer.parseInt(ra.getContent().toString()));
328                }
329
330                ra = ref.get("minEvictableIdleTimeMillis");
331                if (ra != null && ra.getContent() != null) {
332                    setMinEvictableIdleTimeMillis(
333                        Integer.parseInt(ra.getContent().toString()));
334                }
335                ra = ref.get("maxPreparedStatements");
336                if (ra != null && ra.getContent() != null) {
337                    setMaxPreparedStatements(
338                        Integer.parseInt(ra.getContent().toString()));
339                }
340
341                ra = ref.get("accessToUnderlyingConnectionAllowed");
342                if (ra != null && ra.getContent() != null) {
343                    setAccessToUnderlyingConnectionAllowed(
344                            Boolean.valueOf(ra.getContent().toString()).booleanValue());
345                }
346
347                cpds = this;
348            }
349        }
350        return cpds;
351    }
352
353    /**
354     * Throws an IllegalStateException, if a PooledConnection has already
355     * been requested.
356     */
357    private void assertInitializationAllowed() throws IllegalStateException {
358        if (getConnectionCalled) {
359            throw new IllegalStateException(GET_CONNECTION_CALLED);
360        }
361    }
362
363    // ----------------------------------------------------------------------
364    // Properties
365
366    /**
367     * Get the connection properties passed to the JDBC driver.
368     *
369     * @return the JDBC connection properties used when creating connections.
370     */
371    public Properties getConnectionProperties() {
372        return connectionProperties;
373    }
374
375    /**
376     * <p>Set the connection properties passed to the JDBC driver.</p>
377     *
378     * <p>If <code>props</code> contains "user" and/or "password"
379     * properties, the corresponding instance properties are set. If these
380     * properties are not present, they are filled in using
381     * {@link #getUser()}, {@link #getPassword()} when {@link #getPooledConnection()}
382     * is called, or using the actual parameters to the method call when
383     * {@link #getPooledConnection(String, String)} is called. Calls to
384     * {@link #setUser(String)} or {@link #setPassword(String)} overwrite the values
385     * of these properties if <code>connectionProperties</code> is not null.</p>
386     *
387     * @param props Connection properties to use when creating new connections.
388     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
389     */
390    public void setConnectionProperties(Properties props) {
391        assertInitializationAllowed();
392        connectionProperties = props;
393        if (connectionProperties.containsKey("user")) {
394            setUser(connectionProperties.getProperty("user"));
395        }
396        if (connectionProperties.containsKey("password")) {
397            setPassword(connectionProperties.getProperty("password"));
398        }
399    }
400
401    /**
402     * Get the value of description.  This property is here for use by
403     * the code which will deploy this datasource.  It is not used
404     * internally.
405     *
406     * @return value of description, may be null.
407     * @see #setDescription(String)
408     */
409    public String getDescription() {
410        return description;
411    }
412
413    /**
414     * Set the value of description.  This property is here for use by
415     * the code which will deploy this datasource.  It is not used
416     * internally.
417     *
418     * @param v  Value to assign to description.
419     */
420    public void setDescription(String  v) {
421        this.description = v;
422    }
423
424    /**
425     * Get the value of password for the default user.
426     * @return value of password.
427     */
428    public String getPassword() {
429        return password;
430    }
431
432    /**
433     * Set the value of password for the default user.
434     * @param v  Value to assign to password.
435     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
436     */
437    public void setPassword(String v) {
438        assertInitializationAllowed();
439        this.password = v;
440        if (connectionProperties != null) {
441            connectionProperties.setProperty("password", v);
442        }
443    }
444
445    /**
446     * Get the value of url used to locate the database for this datasource.
447     * @return value of url.
448     */
449    public String getUrl() {
450        return url;
451    }
452
453    /**
454     * Set the value of url used to locate the database for this datasource.
455     * @param v  Value to assign to url.
456     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
457    */
458    public void setUrl(String v) {
459        assertInitializationAllowed();
460        this.url = v;
461    }
462
463    /**
464     * Get the value of default user (login or username).
465     * @return value of user.
466     */
467    public String getUser() {
468        return user;
469    }
470
471    /**
472     * Set the value of default user (login or username).
473     * @param v  Value to assign to user.
474     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
475     */
476    public void setUser(String v) {
477        assertInitializationAllowed();
478        this.user = v;
479        if (connectionProperties != null) {
480            connectionProperties.setProperty("user", v);
481        }
482    }
483
484    /**
485     * Get the driver classname.
486     * @return value of driver.
487     */
488    public String getDriver() {
489        return driver;
490    }
491
492    /**
493     * Set the driver classname.  Setting the driver classname cause the
494     * driver to be registered with the DriverManager.
495     * @param v  Value to assign to driver.
496     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
497     */
498    public void setDriver(String v) throws ClassNotFoundException {
499        assertInitializationAllowed();
500        this.driver = v;
501        // make sure driver is registered
502        Class.forName(v);
503    }
504
505    /**
506     * Gets the maximum time in seconds that this data source can wait
507     * while attempting to connect to a database. NOT USED.
508     */
509    @Override
510    public int getLoginTimeout() {
511        return loginTimeout;
512    }
513
514    /**
515     * Get the log writer for this data source. NOT USED.
516     */
517    @Override
518    public PrintWriter getLogWriter() {
519        return logWriter;
520    }
521
522    /**
523     * Sets the maximum time in seconds that this data source will wait
524     * while attempting to connect to a database. NOT USED.
525     */
526    @Override
527    public void setLoginTimeout(int seconds) {
528        loginTimeout = seconds;
529    }
530
531    /**
532     * Set the log writer for this data source. NOT USED.
533     */
534    @Override
535    public void setLogWriter(PrintWriter out) {
536        logWriter = out;
537    }
538
539
540    // ------------------------------------------------------------------
541    // PreparedStatement pool properties
542
543
544    /**
545     * Flag to toggle the pooling of <code>PreparedStatement</code>s
546     * @return value of poolPreparedStatements.
547     */
548    public boolean isPoolPreparedStatements() {
549        return poolPreparedStatements;
550    }
551
552    /**
553     * Flag to toggle the pooling of <code>PreparedStatement</code>s
554     * @param v  true to pool statements.
555     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
556     */
557    public void setPoolPreparedStatements(boolean v) {
558        assertInitializationAllowed();
559        this.poolPreparedStatements = v;
560    }
561
562    /**
563     * The maximum number of statements that can remain idle in the
564     * pool, without extra ones being released, or negative for no limit.
565     * @return the value of maxIdle
566     */
567    public int getMaxIdle() {
568        return this.maxIdle;
569    }
570
571    /**
572     * The maximum number of statements that can remain idle in the
573     * pool, without extra ones being released, or negative for no limit.
574     *
575     * @param maxIdle The maximum number of statements that can remain idle
576     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
577     */
578    public void setMaxIdle(int maxIdle) {
579        assertInitializationAllowed();
580        this.maxIdle = maxIdle;
581    }
582
583    /**
584     * Returns the number of milliseconds to sleep between runs of the
585     * idle object evictor thread.
586     * When non-positive, no idle object evictor thread will be
587     * run.
588     * @return the value of the evictor thread timer
589     * @see #setTimeBetweenEvictionRunsMillis(long)
590     */
591    public long getTimeBetweenEvictionRunsMillis() {
592        return _timeBetweenEvictionRunsMillis;
593    }
594
595    /**
596     * Sets the number of milliseconds to sleep between runs of the
597     * idle object evictor thread.
598     * When non-positive, no idle object evictor thread will be
599     * run.
600     * @param timeBetweenEvictionRunsMillis
601     * @see #getTimeBetweenEvictionRunsMillis()
602     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
603     */
604    public void setTimeBetweenEvictionRunsMillis(
605            long timeBetweenEvictionRunsMillis) {
606        assertInitializationAllowed();
607        _timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
608    }
609
610    /**
611     * Returns the number of statements to examine during each run of the
612     * idle object evictor thread (if any).
613     *
614     * *see #setNumTestsPerEvictionRun
615     * *see #setTimeBetweenEvictionRunsMillis
616     */
617    public int getNumTestsPerEvictionRun() {
618        return _numTestsPerEvictionRun;
619    }
620
621    /**
622     * Sets the number of statements to examine during each run of the
623     * idle object evictor thread (if any).
624     * <p>
625     * When a negative value is supplied, <tt>ceil({*link #numIdle})/abs({*link #getNumTestsPerEvictionRun})</tt>
626     * tests will be run.  I.e., when the value is <i>-n</i>, roughly one <i>n</i>th of the
627     * idle objects will be tested per run.
628     *
629     * @param numTestsPerEvictionRun number of statements to examine per run
630     * @see #getNumTestsPerEvictionRun()
631     * @see #setTimeBetweenEvictionRunsMillis(long)
632     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
633     */
634    public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
635        assertInitializationAllowed();
636        _numTestsPerEvictionRun = numTestsPerEvictionRun;
637    }
638
639    /**
640     * Returns the minimum amount of time a statement may sit idle in the pool
641     * before it is eligible for eviction by the idle object evictor
642     * (if any).
643     *
644     * *see #setMinEvictableIdleTimeMillis
645     * *see #setTimeBetweenEvictionRunsMillis
646     */
647    public int getMinEvictableIdleTimeMillis() {
648        return _minEvictableIdleTimeMillis;
649    }
650
651    /**
652     * Sets the minimum amount of time a statement may sit idle in the pool
653     * before it is eligible for eviction by the idle object evictor
654     * (if any).
655     * When non-positive, no objects will be evicted from the pool
656     * due to idle time alone.
657     * @param minEvictableIdleTimeMillis minimum time to set (in ms)
658     * @see #getMinEvictableIdleTimeMillis()
659     * @see #setTimeBetweenEvictionRunsMillis(long)
660     * @throws IllegalStateException if {@link #getPooledConnection()} has been called
661     */
662    public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
663        assertInitializationAllowed();
664        _minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
665    }
666
667    /**
668     * Returns the value of the accessToUnderlyingConnectionAllowed property.
669     *
670     * @return true if access to the underlying is allowed, false otherwise.
671     */
672    public synchronized boolean isAccessToUnderlyingConnectionAllowed() {
673        return this.accessToUnderlyingConnectionAllowed;
674    }
675
676    /**
677     * Sets the value of the accessToUnderlyingConnectionAllowed property.
678     * It controls if the PoolGuard allows access to the underlying connection.
679     * (Default: false)
680     *
681     * @param allow Access to the underlying connection is granted when true.
682     */
683    public synchronized void setAccessToUnderlyingConnectionAllowed(boolean allow) {
684        this.accessToUnderlyingConnectionAllowed = allow;
685    }
686
687    /**
688     * Returns the maximun number of prepared statements.
689     *
690     * @return maxPrepartedStatements value
691     */
692    public int getMaxPreparedStatements()
693    {
694        return _maxPreparedStatements;
695    }
696
697    /**
698     * Sets the maximum number of prepared statements.
699     * @param maxPreparedStatements the new maximum number of prepared
700     * statements
701     */
702    public void setMaxPreparedStatements(int maxPreparedStatements)
703    {
704        _maxPreparedStatements = maxPreparedStatements;
705    }
706}