001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2001-2005 Julian Hyde and others
008// Copyright (C) 2005-2012 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap;
012
013import mondrian.olap.Util;
014import mondrian.resource.MondrianResource;
015import mondrian.rolap.aggmatcher.JdbcSchema;
016import mondrian.spi.DynamicSchemaProcessor;
017import mondrian.util.*;
018
019import org.apache.log4j.Logger;
020
021import java.io.IOException;
022import java.lang.ref.*;
023import java.lang.reflect.Constructor;
024import java.util.*;
025
026import javax.sql.DataSource;
027
028/**
029 * A collection of schemas, identified by their connection properties
030 * (catalog name, JDBC URL, and so forth).
031 *
032 * <p>To lookup a schema, call
033 * <code>RolapSchemaPool.{@link #instance}().{@link #get}</code>.</p>
034 */
035class RolapSchemaPool {
036    static final Logger LOGGER = Logger.getLogger(RolapSchemaPool.class);
037
038    private static final RolapSchemaPool INSTANCE = new RolapSchemaPool();
039
040    private final Map<SchemaKey, ExpiringReference<RolapSchema>>
041        mapKeyToSchema =
042            new HashMap<SchemaKey, ExpiringReference<RolapSchema>>();
043
044    // REVIEW: This map is now considered unsafe. If two schemas have identical
045    // metadata but a different underlying database connection, we should not
046    // share a cache. Since SchemaContentKey is now a hash of the schema
047    // definition, this field can probably be removed.
048    private final Map<ByteString, ExpiringReference<RolapSchema>>
049        mapMd5ToSchema =
050            new HashMap<ByteString, ExpiringReference<RolapSchema>>();
051
052    private RolapSchemaPool() {
053    }
054
055    static RolapSchemaPool instance() {
056        return INSTANCE;
057    }
058
059    synchronized RolapSchema get(
060        final String catalogUrl,
061        final String connectionKey,
062        final String jdbcUser,
063        final String dataSourceStr,
064        final Util.PropertyList connectInfo)
065    {
066        return get(
067            catalogUrl,
068            connectionKey,
069            jdbcUser,
070            dataSourceStr,
071            null,
072            connectInfo);
073    }
074
075    synchronized RolapSchema get(
076        final String catalogUrl,
077        final DataSource dataSource,
078        final Util.PropertyList connectInfo)
079    {
080        return get(
081            catalogUrl,
082            null,
083            null,
084            null,
085            dataSource,
086            connectInfo);
087    }
088
089    private RolapSchema get(
090        final String catalogUrl,
091        final String connectionKey,
092        final String jdbcUser,
093        final String dataSourceStr,
094        final DataSource dataSource,
095        final Util.PropertyList connectInfo)
096    {
097        final String connectionUuidStr = connectInfo.get(
098            RolapConnectionProperties.JdbcConnectionUuid.name());
099        final boolean useSchemaPool =
100            Boolean.parseBoolean(
101                connectInfo.get(
102                    RolapConnectionProperties.UseSchemaPool.name(),
103                    "true"));
104        final String pinSchemaTimeout =
105            connectInfo.get(
106                RolapConnectionProperties.PinSchemaTimeout.name(),
107                "-1s");
108        final boolean useContentChecksum =
109            Boolean.parseBoolean(
110                connectInfo.get(
111                    RolapConnectionProperties.UseContentChecksum.name()));
112        if (LOGGER.isDebugEnabled()) {
113            LOGGER.debug(
114                "get: catalog=" + catalogUrl
115                + ", connectionKey=" + connectionKey
116                + ", jdbcUser=" + jdbcUser
117                + ", dataSourceStr=" + dataSourceStr
118                + ", dataSource=" + dataSource
119                + ", jdbcConnectionUuid=" + connectionUuidStr
120                + ", useSchemaPool=" + useSchemaPool
121                + ", useContentChecksum=" + useContentChecksum
122                + ", map-size=" + mapKeyToSchema.size()
123                + ", md5-map-size=" + mapMd5ToSchema.size());
124        }
125        final ConnectionKey connectionKey1 =
126            ConnectionKey.create(
127                connectionUuidStr,
128                dataSource,
129                catalogUrl,
130                connectionKey,
131                jdbcUser,
132                dataSourceStr);
133
134        final String catalogStr = getSchemaContent(connectInfo, catalogUrl);
135        final SchemaContentKey schemaContentKey =
136            SchemaContentKey.create(connectInfo, catalogUrl, catalogStr);
137        final SchemaKey key =
138            new SchemaKey(
139                schemaContentKey,
140                connectionKey1);
141
142        // Use the schema pool unless "UseSchemaPool" is explicitly false.
143        RolapSchema schema = null;
144        if (!useSchemaPool) {
145            schema =
146                new RolapSchema(
147                    key,
148                    null,
149                    catalogUrl,
150                    catalogStr,
151                    connectInfo,
152                    dataSource);
153            if (LOGGER.isDebugEnabled()) {
154                LOGGER.debug(
155                    "create (no pool): schema-name=" + schema.getName()
156                    + ", schema-id="
157                    + Integer.toHexString(System.identityHashCode(schema)));
158            }
159            return schema;
160        }
161
162        if (useContentChecksum) {
163            final ByteString md5Bytes =
164                new ByteString(Util.digestMd5(catalogStr));
165            final ExpiringReference<RolapSchema> ref =
166                mapMd5ToSchema.get(md5Bytes);
167            if (LOGGER.isDebugEnabled()) {
168                LOGGER.debug(
169                    "get(key=" + key
170                    + ") returned " + toString(ref));
171            }
172
173            if (ref != null) {
174                schema = ref.get(pinSchemaTimeout);
175                if (schema == null) {
176                    // clear out the reference since schema is null
177                    mapKeyToSchema.remove(key);
178                    mapMd5ToSchema.remove(md5Bytes);
179                }
180            }
181
182            if (schema == null) {
183                schema = new RolapSchema(
184                    key,
185                    md5Bytes,
186                    catalogUrl,
187                    catalogStr,
188                    connectInfo,
189                    dataSource);
190                if (LOGGER.isDebugEnabled()) {
191                    LOGGER.debug(
192                        "create: schema-name=" + schema.getName()
193                        + ", schema-id=" + System.identityHashCode(schema));
194                }
195                putSchema(schema, md5Bytes, pinSchemaTimeout);
196            }
197            return schema;
198        }
199
200        ExpiringReference<RolapSchema> ref = mapKeyToSchema.get(key);
201        if (LOGGER.isDebugEnabled()) {
202            LOGGER.debug(
203                "get(key=" + key
204                + ") returned " + toString(ref));
205        }
206        if (ref != null) {
207            schema = ref.get(pinSchemaTimeout);
208            if (schema == null) {
209                mapKeyToSchema.remove(key);
210            }
211        }
212
213        if (schema == null) {
214            schema = new RolapSchema(
215                key,
216                null,
217                catalogUrl,
218                catalogStr,
219                connectInfo,
220                dataSource);
221            if (LOGGER.isDebugEnabled()) {
222                LOGGER.debug("create: " + schema);
223            }
224            putSchema(schema, null, pinSchemaTimeout);
225        }
226
227        return schema;
228    }
229
230    private void putSchema(
231        final RolapSchema schema,
232        final ByteString md5Bytes,
233        final String pinTimeout)
234    {
235        final ExpiringReference<RolapSchema> reference =
236            new ExpiringReference<RolapSchema>(
237                schema, pinTimeout);
238        if (md5Bytes != null) {
239            mapMd5ToSchema.put(md5Bytes, reference);
240        }
241        mapKeyToSchema.put(schema.key, reference);
242
243        if (LOGGER.isDebugEnabled()) {
244            LOGGER.debug(
245                "put: schema=" + schema
246                + ", key=" + schema.key
247                + ", checksum=" + md5Bytes
248                + ", map-size=" + mapKeyToSchema.size()
249                + ", md5-map-size=" + mapMd5ToSchema.size());
250        }
251    }
252
253    private static String getSchemaContent(
254        final Util.PropertyList connectInfo,
255        final String catalogUrl)
256    {
257        // We will return the first of the following:
258        //  1. CatalogContent property if set
259        //  2. DynamicSchemaProcessor#processSchema if set
260        //  3. Util.readVirtualFileAsString(catalogUrl)
261
262        String catalogStr = connectInfo.get(
263            RolapConnectionProperties.CatalogContent.name());
264
265        if (Util.isEmpty(catalogStr)) {
266            if (Util.isEmpty(catalogUrl)) {
267                throw MondrianResource.instance()
268                    .ConnectStringMandatoryProperties.ex(
269                        RolapConnectionProperties.Catalog.name(),
270                        RolapConnectionProperties.CatalogContent.name());
271            }
272            // check for a DynamicSchemaProcessor
273            String dynProcName = connectInfo.get(
274                RolapConnectionProperties.DynamicSchemaProcessor.name());
275            if (!Util.isEmpty(dynProcName)) {
276                catalogStr =
277                    processDynamicSchema(
278                        dynProcName, catalogUrl, connectInfo);
279            }
280
281            if (Util.isEmpty(catalogStr)) {
282                // read schema from file
283                try {
284                    catalogStr = Util.readVirtualFileAsString(catalogUrl);
285                } catch (IOException e) {
286                    throw Util.newError(
287                        e,
288                        "loading schema from url " + catalogUrl);
289                }
290            }
291        }
292
293        return catalogStr;
294    }
295
296    private static String processDynamicSchema(
297        final String dynProcName,
298        final String catalogUrl,
299        final Util.PropertyList connectInfo)
300    {
301        if (RolapSchema.LOGGER.isDebugEnabled()) {
302            RolapSchema.LOGGER.debug(
303                "Pool.get: create schema \"" + catalogUrl
304                + "\" using dynamic processor");
305        }
306        try {
307            final DynamicSchemaProcessor dynProc =
308                ClassResolver.INSTANCE.instantiateSafe(dynProcName);
309            return dynProc.processSchema(catalogUrl, connectInfo);
310        } catch (Exception e) {
311            throw Util.newError(
312                e,
313                "loading DynamicSchemaProcessor " + dynProcName);
314        }
315    }
316
317    synchronized void remove(
318        final String catalogUrl,
319        final String connectionKey,
320        final String jdbcUser,
321        final String dataSourceStr)
322    {
323        final SchemaContentKey schemaContentKey =
324            SchemaContentKey.create(
325                new Util.PropertyList(),
326                catalogUrl,
327                null);
328        final ConnectionKey connectionUuid =
329            ConnectionKey.create(
330                null,
331                null,
332                catalogUrl,
333                connectionKey,
334                jdbcUser,
335                dataSourceStr);
336        final SchemaKey key =
337            new SchemaKey(schemaContentKey, connectionUuid);
338        if (RolapSchema.LOGGER.isDebugEnabled()) {
339            RolapSchema.LOGGER.debug(
340                "Pool.remove: schema \"" + catalogUrl
341                + "\" and datasource string \"" + dataSourceStr + "\"");
342        }
343        remove(key);
344    }
345
346    synchronized void remove(
347        final String catalogUrl,
348        final DataSource dataSource)
349    {
350        final SchemaContentKey schemaContentKey =
351            SchemaContentKey.create(
352                new Util.PropertyList(),
353                catalogUrl,
354                null);
355        final ConnectionKey connectionKey =
356            ConnectionKey.create(
357                null,
358                dataSource,
359                catalogUrl,
360                null,
361                null,
362                null);
363        final SchemaKey key =
364            new SchemaKey(schemaContentKey, connectionKey);
365        if (RolapSchema.LOGGER.isDebugEnabled()) {
366            RolapSchema.LOGGER.debug(
367                "Pool.remove: schema \"" + catalogUrl
368                + "\" and datasource object");
369        }
370        remove(key);
371    }
372
373    synchronized void remove(RolapSchema schema) {
374        if (schema != null) {
375            if (RolapSchema.LOGGER.isDebugEnabled()) {
376                RolapSchema.LOGGER.debug(
377                    "Pool.remove: schema \"" + schema.getName()
378                    + "\" and datasource object");
379            }
380            remove(schema.key);
381        }
382    }
383
384    private void remove(SchemaKey key) {
385        Reference<RolapSchema> ref = mapKeyToSchema.get(key);
386        if (ref != null) {
387            RolapSchema schema = ref.get();
388            if (schema != null) {
389                mapMd5ToSchema.remove(schema.getChecksum());
390                schema.finalCleanUp();
391            }
392        }
393        mapKeyToSchema.remove(key);
394    }
395
396    synchronized void clear() {
397        if (RolapSchema.LOGGER.isDebugEnabled()) {
398            RolapSchema.LOGGER.debug("Pool.clear: clearing all RolapSchemas");
399        }
400
401        for (Reference<RolapSchema> ref : mapKeyToSchema.values()) {
402            if (ref != null) {
403                RolapSchema schema = ref.get();
404                if (schema != null) {
405                    schema.finalCleanUp();
406                }
407            }
408        }
409        mapKeyToSchema.clear();
410        mapMd5ToSchema.clear();
411        JdbcSchema.clearAllDBs();
412    }
413
414    /**
415     * Returns a list of schemas in this pool.
416     *
417     * @return List of schemas in this pool
418     */
419    synchronized List<RolapSchema> getRolapSchemas() {
420        List<RolapSchema> list = new ArrayList<RolapSchema>();
421        for (RolapSchema schema
422            : Util.GcIterator.over(mapKeyToSchema.values()))
423        {
424            list.add(schema);
425        }
426        return list;
427    }
428
429    synchronized boolean contains(RolapSchema rolapSchema) {
430        return mapKeyToSchema.containsKey(rolapSchema.key);
431    }
432
433    private static <T> String toString(Reference<T> ref) {
434        if (ref == null) {
435            return "null";
436        } else {
437            T t = ref.get();
438            if (t == null) {
439                return "ref(null)";
440            } else {
441                return "ref(" + t
442                    + ", id=" + Integer.toHexString(System.identityHashCode(t))
443                    + ")";
444            }
445        }
446    }
447}
448
449// End RolapSchemaPool.java