Author: aadamchik
Date: Thu Jun 29 10:29:19 2006
New Revision: 418092
URL: http://svn.apache.org/viewvc?rev=418092&view=rev
Log:
demo of concurrent access to the embedded derby (or any other embeddable)
database
Added:
incubator/cayenne/sandbox/derby-pool/.classpath
incubator/cayenne/sandbox/derby-pool/.project
incubator/cayenne/sandbox/derby-pool/lib/
incubator/cayenne/sandbox/derby-pool/lib/cayenne-1.2RC2.jar (with props)
incubator/cayenne/sandbox/derby-pool/lib/derby-10.1.2.1.jar (with props)
incubator/cayenne/sandbox/derby-pool/lib/jgroups-2.2.7.jar (with props)
incubator/cayenne/sandbox/derby-pool/src/
incubator/cayenne/sandbox/derby-pool/src/pool/
incubator/cayenne/sandbox/derby-pool/src/pool/ConnectionWrapper.java
incubator/cayenne/sandbox/derby-pool/src/pool/DatabaseShutdownDelegate.java
incubator/cayenne/sandbox/derby-pool/src/pool/DbOwnershipEvent.java
incubator/cayenne/sandbox/derby-pool/src/pool/DerbyShutdownDelegate.java
incubator/cayenne/sandbox/derby-pool/src/pool/Main.java
incubator/cayenne/sandbox/derby-pool/src/pool/SharedEmbeddedDataSource.java
Added: incubator/cayenne/sandbox/derby-pool/.classpath
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/.classpath?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/.classpath (added)
+++ incubator/cayenne/sandbox/derby-pool/.classpath Thu Jun 29 10:29:19 2006
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con"
path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="lib" path="lib/derby-10.1.2.1.jar"/>
+ <classpathentry
sourcepath="/Users/andrus/work/cayenne/cayenne-java/src/cayenne/java"
kind="lib" path="lib/cayenne-1.2RC2.jar"/>
+ <classpathentry kind="lib" path="lib/jgroups-2.2.7.jar"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
Added: incubator/cayenne/sandbox/derby-pool/.project
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/.project?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/.project (added)
+++ incubator/cayenne/sandbox/derby-pool/.project Thu Jun 29 10:29:19 2006
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>derby-pool</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
Added: incubator/cayenne/sandbox/derby-pool/lib/cayenne-1.2RC2.jar
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/lib/cayenne-1.2RC2.jar?rev=418092&view=auto
==============================================================================
Binary file - no diff available.
Propchange: incubator/cayenne/sandbox/derby-pool/lib/cayenne-1.2RC2.jar
------------------------------------------------------------------------------
svn:mime-type = application/octet-stream
Added: incubator/cayenne/sandbox/derby-pool/lib/derby-10.1.2.1.jar
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/lib/derby-10.1.2.1.jar?rev=418092&view=auto
==============================================================================
Binary file - no diff available.
Propchange: incubator/cayenne/sandbox/derby-pool/lib/derby-10.1.2.1.jar
------------------------------------------------------------------------------
svn:mime-type = application/octet-stream
Added: incubator/cayenne/sandbox/derby-pool/lib/jgroups-2.2.7.jar
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/lib/jgroups-2.2.7.jar?rev=418092&view=auto
==============================================================================
Binary file - no diff available.
Propchange: incubator/cayenne/sandbox/derby-pool/lib/jgroups-2.2.7.jar
------------------------------------------------------------------------------
svn:mime-type = application/octet-stream
Added: incubator/cayenne/sandbox/derby-pool/src/pool/ConnectionWrapper.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/ConnectionWrapper.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/ConnectionWrapper.java (added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/ConnectionWrapper.java Thu
Jun 29 10:29:19 2006
@@ -0,0 +1,186 @@
+package pool;
+
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.Savepoint;
+import java.sql.Statement;
+import java.util.Map;
+
+/**
+ * Noop connection wrapper.
+ *
+ * @author andrus
+ */
+class ConnectionWrapper implements Connection {
+
+ protected Connection connection;
+
+ public ConnectionWrapper(Connection connection) {
+ this.connection = connection;
+ }
+
+ public void clearWarnings() throws SQLException {
+ connection.clearWarnings();
+ }
+
+ public void close() throws SQLException {
+ connection.close();
+ }
+
+ public void commit() throws SQLException {
+ connection.commit();
+ }
+
+ public Statement createStatement() throws SQLException {
+ return connection.createStatement();
+ }
+
+ public Statement createStatement(int resultSetType,
+ int resultSetConcurrency, int resultSetHoldability)
+ throws SQLException {
+ return connection.createStatement(resultSetType,
resultSetConcurrency,
+ resultSetHoldability);
+ }
+
+ public Statement createStatement(int resultSetType, int
resultSetConcurrency)
+ throws SQLException {
+ return connection.createStatement(resultSetType,
resultSetConcurrency);
+ }
+
+ public boolean getAutoCommit() throws SQLException {
+ return connection.getAutoCommit();
+ }
+
+ public String getCatalog() throws SQLException {
+ return connection.getCatalog();
+ }
+
+ public int getHoldability() throws SQLException {
+ return connection.getHoldability();
+ }
+
+ public DatabaseMetaData getMetaData() throws SQLException {
+ return connection.getMetaData();
+ }
+
+ public int getTransactionIsolation() throws SQLException {
+ return connection.getTransactionIsolation();
+ }
+
+ public Map<String, Class<?>> getTypeMap() throws SQLException {
+ return connection.getTypeMap();
+ }
+
+ public SQLWarning getWarnings() throws SQLException {
+ return connection.getWarnings();
+ }
+
+ public boolean isClosed() throws SQLException {
+ return connection.isClosed();
+ }
+
+ public boolean isReadOnly() throws SQLException {
+ return connection.isReadOnly();
+ }
+
+ public String nativeSQL(String sql) throws SQLException {
+ return connection.nativeSQL(sql);
+ }
+
+ public CallableStatement prepareCall(String sql, int resultSetType,
+ int resultSetConcurrency, int resultSetHoldability)
+ throws SQLException {
+ return connection.prepareCall(sql, resultSetType,
resultSetConcurrency,
+ resultSetHoldability);
+ }
+
+ public CallableStatement prepareCall(String sql, int resultSetType,
+ int resultSetConcurrency) throws SQLException {
+ return connection.prepareCall(sql, resultSetType,
resultSetConcurrency);
+ }
+
+ public CallableStatement prepareCall(String sql) throws SQLException {
+ return connection.prepareCall(sql);
+ }
+
+ public PreparedStatement prepareStatement(String sql, int resultSetType,
+ int resultSetConcurrency, int resultSetHoldability)
+ throws SQLException {
+ return connection.prepareStatement(sql, resultSetType,
+ resultSetConcurrency, resultSetHoldability);
+ }
+
+ public PreparedStatement prepareStatement(String sql, int resultSetType,
+ int resultSetConcurrency) throws SQLException {
+ return connection.prepareStatement(sql, resultSetType,
+ resultSetConcurrency);
+ }
+
+ public PreparedStatement prepareStatement(String sql, int
autoGeneratedKeys)
+ throws SQLException {
+ return connection.prepareStatement(sql, autoGeneratedKeys);
+ }
+
+ public PreparedStatement prepareStatement(String sql, int[]
columnIndexes)
+ throws SQLException {
+ return connection.prepareStatement(sql, columnIndexes);
+ }
+
+ public PreparedStatement prepareStatement(String sql, String[]
columnNames)
+ throws SQLException {
+ return connection.prepareStatement(sql, columnNames);
+ }
+
+ public PreparedStatement prepareStatement(String sql) throws
SQLException {
+ return connection.prepareStatement(sql);
+ }
+
+ public void releaseSavepoint(Savepoint savepoint) throws SQLException {
+ connection.releaseSavepoint(savepoint);
+ }
+
+ public void rollback() throws SQLException {
+ connection.rollback();
+ }
+
+ public void rollback(Savepoint savepoint) throws SQLException {
+ connection.rollback(savepoint);
+ }
+
+ public void setAutoCommit(boolean autoCommit) throws SQLException {
+ connection.setAutoCommit(autoCommit);
+ }
+
+ public void setCatalog(String catalog) throws SQLException {
+ connection.setCatalog(catalog);
+ }
+
+ public void setHoldability(int holdability) throws SQLException {
+ connection.setHoldability(holdability);
+ }
+
+ public void setReadOnly(boolean readOnly) throws SQLException {
+ connection.setReadOnly(readOnly);
+ }
+
+ public Savepoint setSavepoint() throws SQLException {
+ return connection.setSavepoint();
+ }
+
+ public Savepoint setSavepoint(String name) throws SQLException {
+ return connection.setSavepoint(name);
+ }
+
+ public void setTransactionIsolation(int level) throws SQLException {
+ connection.setTransactionIsolation(level);
+ }
+
+ public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+ connection.setTypeMap(map);
+ }
+
+}
Added:
incubator/cayenne/sandbox/derby-pool/src/pool/DatabaseShutdownDelegate.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/DatabaseShutdownDelegate.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/DatabaseShutdownDelegate.java
(added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/DatabaseShutdownDelegate.java
Thu Jun 29 10:29:19 2006
@@ -0,0 +1,12 @@
+package pool;
+
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+public interface DatabaseShutdownDelegate {
+
+ void beforeYieldDbOwnership(DataSource ds) throws SQLException;
+
+ void afterYieldDbOwnership(DataSource ds) throws SQLException;
+}
Added: incubator/cayenne/sandbox/derby-pool/src/pool/DbOwnershipEvent.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/DbOwnershipEvent.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/DbOwnershipEvent.java (added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/DbOwnershipEvent.java Thu Jun
29 10:29:19 2006
@@ -0,0 +1,37 @@
+package pool;
+
+import org.objectstyle.cayenne.event.CayenneEvent;
+
+public class DbOwnershipEvent extends CayenneEvent {
+
+ public static final int REQUEST_OWNERSHIP = 1;
+
+ public static final int OWNERSHIP_RELEASED = 2;
+
+ protected int type;
+
+ protected String senderId;
+
+ protected String referenceId;
+
+ public DbOwnershipEvent(SharedEmbeddedDataSource source, int type,
+ String senderId, String referenceId) {
+ super(source);
+ this.type = type;
+ this.senderId = senderId;
+ this.referenceId = referenceId;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public String getReferenceId() {
+ return referenceId;
+ }
+
+ public String getSenderId() {
+ return senderId;
+ }
+
+}
Added: incubator/cayenne/sandbox/derby-pool/src/pool/DerbyShutdownDelegate.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/DerbyShutdownDelegate.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/DerbyShutdownDelegate.java
(added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/DerbyShutdownDelegate.java
Thu Jun 29 10:29:19 2006
@@ -0,0 +1,31 @@
+package pool;
+
+import java.sql.DriverManager;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+public class DerbyShutdownDelegate implements DatabaseShutdownDelegate {
+
+ protected String url;
+
+ public DerbyShutdownDelegate(String url) {
+ this.url = url;
+ }
+
+ public void beforeYieldDbOwnership(DataSource ds) throws SQLException {
+
+ }
+
+ public void afterYieldDbOwnership(DataSource ds) throws SQLException {
+ try {
+ DriverManager.getConnection(url + ";shutdown=true");
+ } catch (SQLException e) {
+ // exception is expected on shutdown
+ return;
+ }
+
+ throw new SQLException(
+ "Failed to shutdown the database. Likely there
are still open connections");
+ }
+}
Added: incubator/cayenne/sandbox/derby-pool/src/pool/Main.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/Main.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/Main.java (added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/Main.java Thu Jun 29 10:29:19
2006
@@ -0,0 +1,67 @@
+package pool;
+
+import java.sql.Connection;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.objectstyle.cayenne.conf.Configuration;
+import org.objectstyle.cayenne.conn.DriverDataSource;
+
+public class Main {
+
+ public static void main(String[] args) {
+
+ Configuration.configureCommonLogging();
+
+ String url =
"jdbc:derby:/Users/andrus/work/cayenne-photoalbum/photoalbum-swing/photoalbum-resources/derby";
+
+ DataSource pool;
+ try {
+ pool = new
DriverDataSource("org.apache.derby.jdbc.EmbeddedDriver",
+ url);
+ } catch (SQLException e) {
+ throw new RuntimeException("Can't create DS", e);
+ }
+
+ SharedEmbeddedDataSource collaboratingDS = new
SharedEmbeddedDataSource(
+ pool);
+ collaboratingDS.setDelegate(new DerbyShutdownDelegate(url));
+ while (true) {
+
+ try {
+
+ Connection c = collaboratingDS.getConnection();
+ try {
+ PreparedStatement p = c
+
.prepareStatement("select count(1) as ct from ALBUM");
+
+ ResultSet rs = p.executeQuery();
+ rs.next();
+ System.out.println("Count: " +
rs.getInt(1));
+ rs.close();
+ p.close();
+
+ p = c
+
.prepareStatement("insert into ALBUM (NAME, CREATED) values ('X', ?)");
+ p.setDate(1, new
Date(System.currentTimeMillis()));
+ p.executeUpdate();
+ p.close();
+ } finally {
+ c.close();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL Error", e);
+ }
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+
+ }
+ }
+ }
+}
Added:
incubator/cayenne/sandbox/derby-pool/src/pool/SharedEmbeddedDataSource.java
URL:
http://svn.apache.org/viewvc/incubator/cayenne/sandbox/derby-pool/src/pool/SharedEmbeddedDataSource.java?rev=418092&view=auto
==============================================================================
--- incubator/cayenne/sandbox/derby-pool/src/pool/SharedEmbeddedDataSource.java
(added)
+++ incubator/cayenne/sandbox/derby-pool/src/pool/SharedEmbeddedDataSource.java
Thu Jun 29 10:29:19 2006
@@ -0,0 +1,276 @@
+package pool;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+
+import javax.sql.DataSource;
+
+import org.objectstyle.cayenne.event.EventBridge;
+import org.objectstyle.cayenne.event.EventManager;
+import org.objectstyle.cayenne.event.EventSubject;
+import org.objectstyle.cayenne.event.JavaGroupsBridgeFactory;
+import org.objectstyle.cayenne.util.IDUtil;
+
+/**
+ * A collaborating cross-VM DataSource wrapper that can be shut down via
+ * multicast.
+ *
+ * @author andrus
+ */
+public class SharedEmbeddedDataSource implements DataSource {
+
+ static final EventSubject DB_OWNERSHIP_SUBJECT =
EventSubject.getSubject(
+ SharedEmbeddedDataSource.class, "ownershipRequest");
+
+ protected DataSource dataSource;
+
+ protected EventManager eventManager;
+
+ protected EventBridge bridge;
+
+ protected int maxWait;
+
+ protected int maxOpWait;
+
+ protected DatabaseShutdownDelegate delegate;
+
+ protected boolean idle;
+
+ protected boolean owner;
+
+ protected Object requestLock;
+
+ protected String poolId;
+
+ static String makeId() {
+ byte[] bytes = IDUtil.pseudoUniqueByteSequence8();
+ StringBuffer buffer = new StringBuffer(8);
+ for (int i = 0; i < 8; i++) {
+ IDUtil.appendFormattedByte(buffer, bytes[i]);
+ }
+
+ return buffer.toString();
+ }
+
+ public SharedEmbeddedDataSource(DataSource dataSource) {
+ this.poolId = makeId();
+ this.dataSource = dataSource;
+ this.maxWait = 6000;
+ this.maxOpWait = 1000;
+ this.eventManager = new EventManager(2);
+ this.idle = true;
+ this.owner = false;
+ this.requestLock = new Object();
+
+ // startup cross-VM bridge and a local listener for the events
+
+ this.bridge = new JavaGroupsBridgeFactory().createEventBridge(
+ Collections.singleton(DB_OWNERSHIP_SUBJECT),
+ DB_OWNERSHIP_SUBJECT.getSubjectName(),
Collections.EMPTY_MAP);
+
+ eventManager.addNonBlockingListener(this,
"processDbOwnershipEvent",
+ DbOwnershipEvent.class, DB_OWNERSHIP_SUBJECT,
bridge);
+ try {
+ bridge.startup(eventManager,
EventBridge.RECEIVE_LOCAL_EXTERNAL);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public synchronized Connection getConnection() throws SQLException {
+ ensureIdle();
+
+ SQLException exception = null;
+
+ for (int i = 0; i < 5; i++) {
+ requestDbOwnership();
+
+ try {
+ return
wrapConnection(dataSource.getConnection());
+ } catch (SQLException e) {
+ System.out.println("Retrying...");
+ owner = false;
+
+ // this is needed to prevent events flood
+ try {
+ wait(maxOpWait);
+ } catch (InterruptedException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ exception = e;
+ }
+ }
+
+ throw exception;
+ }
+
+ public synchronized Connection getConnection(String userName,
+ String password) throws SQLException {
+ ensureIdle();
+ requestDbOwnership();
+
+ try {
+ return
wrapConnection(dataSource.getConnection(userName, password));
+ } catch (SQLException e) {
+ owner = false;
+ throw e;
+ }
+ }
+
+ public PrintWriter getLogWriter() throws SQLException {
+ return dataSource.getLogWriter();
+ }
+
+ public int getLoginTimeout() throws SQLException {
+ return dataSource.getLoginTimeout();
+ }
+
+ public void setLogWriter(PrintWriter writer) throws SQLException {
+ dataSource.setLogWriter(writer);
+ }
+
+ public void setLoginTimeout(int seconds) throws SQLException {
+ dataSource.setLoginTimeout(seconds);
+ }
+
+ public DatabaseShutdownDelegate getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(DatabaseShutdownDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ private Connection wrapConnection(Connection c) {
+ idle = false;
+
+ return new ConnectionWrapper(c) {
+ @Override
+ public void close() throws SQLException {
+ // TODO Auto-generated method stub
+ super.close();
+
+ synchronized (this) {
+ idle = true;
+ notifyAll();
+ }
+ }
+ };
+ }
+
+ /**
+ * Shuts down all database connections and the database itself, yelding
the
+ * ownership of the db to some other process.
+ */
+ public void processDbOwnershipEvent(DbOwnershipEvent e) throws
SQLException {
+ switch (e.getType()) {
+ case DbOwnershipEvent.OWNERSHIP_RELEASED:
+ synchronized (requestLock) {
+ if (poolId.equals(e.getReferenceId())) {
+ System.out.println("Got ownership");
+ } else {
+ System.out.println("Stolen");
+ }
+
+ requestLock.notifyAll();
+ }
+ break;
+
+ case DbOwnershipEvent.REQUEST_OWNERSHIP:
+ yield(e.getSenderId());
+ }
+ }
+
+ private synchronized void yield(String id) throws SQLException {
+
+ if (!owner) {
+ return;
+ }
+
+ // make sure the connection is closed
+
+ if (!idle) {
+ try {
+ wait(maxWait);
+ } catch (InterruptedException e) {
+
+ }
+
+ // check ownership again as we released the lock above
+ if (!owner) {
+ return;
+ }
+
+ if (!idle) {
+ System.out.println("**** Connection is busy");
+ return;
+ }
+ }
+
+ System.out.println("Yield");
+ if (delegate != null) {
+ delegate.beforeYieldDbOwnership(this);
+ }
+
+ if (delegate != null) {
+ delegate.afterYieldDbOwnership(this);
+ }
+
+ owner = false;
+ System.out.println("Yield done.");
+ eventManager.postEvent(new DbOwnershipEvent(this,
+ DbOwnershipEvent.OWNERSHIP_RELEASED, poolId,
id),
+ DB_OWNERSHIP_SUBJECT);
+ }
+
+ private void ensureIdle() throws SQLException {
+ if (!idle) {
+ try {
+ wait(maxOpWait);
+ } catch (InterruptedException e) {
+
+ }
+
+ if (!idle) {
+ throw new SQLException("No connections
available");
+ }
+ }
+ }
+
+ private void requestDbOwnership() throws SQLException {
+
+ if (!owner) {
+ long t0 = System.currentTimeMillis();
+
+ // request ownership keeping the lock on this to
prevent yield while
+ // we are waiting,
+ synchronized (requestLock) {
+
+ if (owner) {
+ return;
+ }
+
+ System.out.println("Ownership request sent...");
+
+ DbOwnershipEvent event = new
DbOwnershipEvent(this,
+
DbOwnershipEvent.REQUEST_OWNERSHIP, poolId, null);
+ eventManager.postEvent(event,
DB_OWNERSHIP_SUBJECT);
+
+ // do not release
+ try {
+ requestLock.wait(maxWait);
+ } catch (InterruptedException e) {
+
+ }
+
+ }
+
+ long t1 = System.currentTimeMillis();
+ System.out.println("Ownership request processed in..."
+ (t1 - t0));
+ owner = true;
+ }
+ }
+}