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