001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 */
012
013package org.nuxeo.ecm.core.storage.sql.jdbc;
014
015import java.sql.Connection;
016import java.sql.SQLException;
017import java.util.Map.Entry;
018
019import javax.naming.NamingException;
020import javax.sql.DataSource;
021import javax.sql.XAConnection;
022import javax.sql.XADataSource;
023
024import org.apache.commons.beanutils.BeanUtils;
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.nuxeo.ecm.core.api.NuxeoException;
028import org.nuxeo.ecm.core.storage.sql.ClusterInvalidator;
029import org.nuxeo.ecm.core.storage.sql.Mapper;
030import org.nuxeo.ecm.core.storage.sql.Model;
031import org.nuxeo.ecm.core.storage.sql.Model.IdType;
032import org.nuxeo.ecm.core.storage.sql.ModelSetup;
033import org.nuxeo.ecm.core.storage.sql.RepositoryBackend;
034import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor;
035import org.nuxeo.ecm.core.storage.sql.RepositoryImpl;
036import org.nuxeo.ecm.core.storage.sql.Session.PathResolver;
037import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
038import org.nuxeo.runtime.api.Framework;
039import org.nuxeo.runtime.datasource.ConnectionHelper;
040import org.nuxeo.runtime.datasource.DataSourceHelper;
041import org.nuxeo.runtime.datasource.PooledDataSourceRegistry.PooledDataSource;
042
043/**
044 * JDBC Backend for a repository.
045 */
046public class JDBCBackend implements RepositoryBackend {
047
048    private static final Log log = LogFactory.getLog(JDBCBackend.class);
049
050    private RepositoryImpl repository;
051
052    private String pseudoDataSourceName;
053
054    private XADataSource xadatasource;
055
056    private Dialect dialect;
057
058    private SQLInfo sqlInfo;
059
060    private boolean firstMapper = true;
061
062    private ClusterInvalidator clusterInvalidator;
063
064    private boolean isPooledDataSource;
065
066    @Override
067    public void initialize(RepositoryImpl repository) {
068        this.repository = repository;
069        RepositoryDescriptor repositoryDescriptor = repository.getRepositoryDescriptor();
070        pseudoDataSourceName = ConnectionHelper.getPseudoDataSourceNameForRepository(repositoryDescriptor.name);
071
072        try {
073            DataSource ds = DataSourceHelper.getDataSource(pseudoDataSourceName);
074            if (ds instanceof PooledDataSource) {
075                isPooledDataSource = true;
076                return;
077            }
078        } catch (NamingException cause) {;
079        }
080
081        // try single-datasource non-XA mode
082        try (Connection connection = ConnectionHelper.getConnection(pseudoDataSourceName)) {
083            if (connection != null) {
084                return;
085            }
086        } catch (SQLException cause) {
087            throw new NuxeoException("Connection error", cause);
088        }
089
090        // standard XA mode
091        // instantiate the XA datasource
092        String className = repositoryDescriptor.xaDataSourceName;
093        Class<?> klass;
094        try {
095            klass = Class.forName(className);
096        } catch (ClassNotFoundException e) {
097            throw new NuxeoException("Unknown class: " + className, e);
098        }
099        Object instance;
100        try {
101            instance = klass.newInstance();
102        } catch (ReflectiveOperationException e) {
103            throw new NuxeoException("Cannot instantiate class: " + className, e);
104        }
105        if (!(instance instanceof XADataSource)) {
106            throw new NuxeoException("Not a XADataSource: " + className);
107        }
108        xadatasource = (XADataSource) instance;
109
110        // set JavaBean properties on the datasource
111        for (Entry<String, String> entry : repositoryDescriptor.properties.entrySet()) {
112            String name = entry.getKey();
113            Object value = Framework.expandVars(entry.getValue());
114            if (name.contains("/")) {
115                // old syntax where non-String types were explicited
116                name = name.substring(0, name.indexOf('/'));
117            }
118            // transform to proper JavaBean convention
119            if (Character.isLowerCase(name.charAt(1))) {
120                name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
121            }
122            try {
123                BeanUtils.setProperty(xadatasource, name, value);
124            } catch (ReflectiveOperationException e) {
125                log.error(String.format("Cannot set %s = %s", name, value));
126            }
127        }
128    }
129
130    /**
131     * {@inheritDoc}
132     * <p>
133     * Opens a connection to get the dialect and finish initializing the {@link ModelSetup}.
134     */
135    @Override
136    public void initializeModelSetup(ModelSetup modelSetup) {
137        try {
138            XAConnection xaconnection = null;
139            // try single-datasource non-XA mode
140            Connection connection = ConnectionHelper.getConnection(pseudoDataSourceName);
141            try {
142                if (connection == null) {
143                    // standard XA mode
144                    xaconnection = xadatasource.getXAConnection();
145                    connection = xaconnection.getConnection();
146                }
147                dialect = Dialect.createDialect(connection, repository.getRepositoryDescriptor());
148            } finally {
149                if (connection != null) {
150                    connection.close();
151                }
152                if (xaconnection != null) {
153                    xaconnection.close();
154                }
155            }
156        } catch (SQLException cause) {
157            throw new NuxeoException("Cannot connect to database", cause);
158        }
159        modelSetup.materializeFulltextSyntheticColumn = dialect.getMaterializeFulltextSyntheticColumn();
160        modelSetup.supportsArrayColumns = dialect.supportsArrayColumns();
161        switch (dialect.getIdType()) {
162        case VARCHAR:
163        case UUID:
164            modelSetup.idType = IdType.STRING;
165            break;
166        case SEQUENCE:
167            modelSetup.idType = IdType.LONG;
168            break;
169        default:
170            throw new AssertionError(dialect.getIdType().toString());
171        }
172    }
173
174    /**
175     * {@inheritDoc}
176     * <p>
177     * Creates the {@link SQLInfo} from the model and the dialect.
178     */
179    @Override
180    public void initializeModel(Model model) {
181        sqlInfo = new SQLInfo(model, dialect);
182    }
183
184    @Override
185    public void setClusterInvalidator(ClusterInvalidator clusterInvalidator) {
186        this.clusterInvalidator = clusterInvalidator;
187    }
188
189    @Override
190    public Mapper newMapper(Model model, PathResolver pathResolver, boolean useInvalidations) {
191        boolean noSharing = !useInvalidations;
192        RepositoryDescriptor repositoryDescriptor = repository.getRepositoryDescriptor();
193
194        ClusterInvalidator cnh = useInvalidations ? clusterInvalidator : null;
195        Mapper mapper = new JDBCMapper(model, pathResolver, sqlInfo, xadatasource, cnh, noSharing, repository);
196        if (isPooledDataSource) {
197            mapper = JDBCMapperConnector.newConnector(mapper);
198            if (noSharing) {
199                mapper = JDBCMapperTxSuspender.newConnector(mapper);
200            }
201        } else {
202            mapper.connect();
203        }
204        if (firstMapper) {
205            firstMapper = false;
206            if (repositoryDescriptor.getNoDDL()) {
207                log.info("Skipping database creation");
208            } else {
209                // first connection, initialize the database
210                mapper.createDatabase();
211            }
212            if (log.isDebugEnabled()) {
213                log.debug(String.format("Database ready, fulltext: disabled=%b searchDisabled=%b.",
214                        repositoryDescriptor.getFulltextDisabled(), repositoryDescriptor.getFulltextSearchDisabled()));
215            }
216        }
217        return mapper;
218    }
219
220    @Override
221    public void shutdown() {
222        if (clusterInvalidator != null) {
223            clusterInvalidator.close();
224        }
225    }
226
227}