001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.store.model;
019    
020    import java.util.ArrayList;
021    import java.util.Collection;
022    import java.util.HashMap;
023    import java.util.HashSet;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.Set;
028    import java.util.StringTokenizer;
029    
030    import org.cumulus4j.store.Cumulus4jStoreManager;
031    import org.cumulus4j.store.UnsupportedDataTypeException;
032    import org.datanucleus.ClassLoaderResolver;
033    import org.datanucleus.ExecutionContext;
034    import org.datanucleus.metadata.AbstractMemberMetaData;
035    import org.datanucleus.metadata.ArrayMetaData;
036    import org.datanucleus.metadata.CollectionMetaData;
037    import org.datanucleus.metadata.MapMetaData;
038    import org.datanucleus.plugin.ConfigurationElement;
039    import org.datanucleus.plugin.PluginManager;
040    import org.datanucleus.util.StringUtils;
041    
042    /**
043     * <p>
044     * Registry responsible for the extension-point <code>org.cumulus4j.store.index_mapping</code>.
045     * </p><p>
046     * This registry maps an {@link IndexEntryFactory} to a java-type or a combination of java-,
047     * jdbc- and sql-type.
048     * </p><p>
049     * There is one instance of <code>IndexEntryFactoryRegistry</code> per {@link Cumulus4jStoreManager}.
050     * </p>
051     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
052     */
053    public class IndexEntryFactoryRegistry
054    {
055            private Cumulus4jStoreManager storeManager;
056    
057            /** Cache of factory for use with each java-type+jdbc+sql */
058            private Map<String, IndexEntryFactory> factoryByKey = new HashMap<String, IndexEntryFactory>();
059    
060            private Map<String, IndexEntryFactory> factoryByEntryType = new HashMap<String, IndexEntryFactory>();
061    
062            /** Mappings of java-type+jdbc+sql type and the factory they should use */
063            private List<IndexMapping> indexMappings = new ArrayList<IndexMapping>();
064    
065            private IndexEntryFactory indexEntryFactoryContainerSize = new DefaultIndexEntryFactory(IndexEntryContainerSize.class);
066    
067            class IndexMapping {
068                    Class<?> javaType;
069                    String jdbcTypes;
070                    String sqlTypes;
071                    IndexEntryFactory factory;
072    
073                    public boolean matches(Class<?> type, String jdbcType, String sqlType) {
074                            if (javaType.isAssignableFrom(type)) {
075                                    if (jdbcTypes != null) {
076                                            if (jdbcType == null) {
077                                                    return false;
078                                            }
079                                            else {
080                                                    return jdbcTypes.indexOf(jdbcType) >= 0;
081                                            }
082                                    }
083                                    else if (sqlTypes != null) {
084                                            if (sqlType == null) {
085                                                    return false;
086                                            }
087                                            else {
088                                                    return sqlTypes.indexOf(sqlType) >= 0;
089                                            }
090                                    }
091                                    else {
092                                            return true;
093                                    }
094                            }
095                            return false;
096                    }
097            }
098    
099            /**
100             * Create a new registry instance.
101             * @param storeMgr the owning store-manager.
102             */
103            public IndexEntryFactoryRegistry(Cumulus4jStoreManager storeMgr)
104            {
105                    this.storeManager = storeMgr;
106    
107                    // Load up plugin information
108                    ClassLoaderResolver clr = storeMgr.getNucleusContext().getClassLoaderResolver(storeMgr.getClass().getClassLoader());
109                    PluginManager pluginMgr = storeMgr.getNucleusContext().getPluginManager();
110                    ConfigurationElement[] elems = pluginMgr.getConfigurationElementsForExtension(
111                                    "org.cumulus4j.store.index_mapping", null, null);
112                    boolean useClob = storeMgr.getBooleanProperty("cumulus4j.index.clob.enabled", true);
113                    if (elems != null) {
114                            for (int i=0;i<elems.length;i++) {
115                                    IndexMapping mapping = new IndexMapping();
116                                    String typeName = elems[i].getAttribute("type");
117                                    mapping.javaType = clr.classForName(typeName);
118    
119                                    String indexTypeName = elems[i].getAttribute("index-entry-type");
120                                    if (indexTypeName != null)
121                                            indexTypeName = indexTypeName.trim();
122    
123                                    if (indexTypeName != null && indexTypeName.isEmpty())
124                                            indexTypeName = null;
125    
126                                    String indexFactoryTypeName = elems[i].getAttribute("index-entry-factory-type");
127                                    if (indexFactoryTypeName != null)
128                                            indexFactoryTypeName = indexFactoryTypeName.trim();
129    
130                                    if (indexFactoryTypeName != null && indexFactoryTypeName.isEmpty())
131                                            indexFactoryTypeName = null;
132    
133                                    if (indexFactoryTypeName != null && indexTypeName != null)
134                                            throw new IllegalStateException("Both, 'index-entry-factory-type' and 'index-entry-type' are specified, but only exactly one must be present! index-entry-factory-type=\"" + indexFactoryTypeName + "\" index-entry-type=\"" + indexTypeName + "\"");
135    
136                                    if (indexFactoryTypeName == null && indexTypeName == null)
137                                            throw new IllegalStateException("Both, 'index-entry-factory-type' and 'index-entry-type' are missing, but exactly one must be present!");
138    
139                                    if (indexFactoryTypeName != null) {
140                                            @SuppressWarnings("unchecked")
141                                            Class<? extends IndexEntryFactory> idxEntryFactoryClass = pluginMgr.loadClass(
142                                                            elems[i].getExtension().getPlugin().getSymbolicName(), indexFactoryTypeName
143                                            );
144                                            try {
145                                                    mapping.factory = idxEntryFactoryClass.newInstance();
146                                            } catch (InstantiationException e) {
147                                                    throw new RuntimeException(e);
148                                            } catch (IllegalAccessException e) {
149                                                    throw new RuntimeException(e);
150                                            }
151                                            indexTypeName = mapping.factory.getIndexEntryClass().getName();
152                                    }
153                                    else {
154                                            if (factoryByEntryType.containsKey(indexTypeName)) {
155                                                    // Reuse the existing factory of this type
156                                                    mapping.factory = factoryByEntryType.get(indexTypeName);
157                                            }
158                                            else {
159                                                    // Create a new factory of this type and cache it
160                                                    @SuppressWarnings("unchecked")
161                                                    Class<? extends IndexEntry> idxEntryClass = pluginMgr.loadClass(
162                                                                    elems[i].getExtension().getPlugin().getSymbolicName(), indexTypeName
163                                                    );
164                                                    IndexEntryFactory factory = new DefaultIndexEntryFactory(idxEntryClass);
165                                                    factoryByEntryType.put(indexTypeName, factory);
166                                                    mapping.factory = factory;
167                                            }
168                                    }
169    
170                                    String jdbcTypes = elems[i].getAttribute("jdbc-types");
171                                    if (!StringUtils.isWhitespace(jdbcTypes)) {
172                                            mapping.jdbcTypes = jdbcTypes;
173                                    }
174                                    String sqlTypes = elems[i].getAttribute("sql-types");
175                                    if (!StringUtils.isWhitespace(sqlTypes)) {
176                                            mapping.sqlTypes = sqlTypes;
177                                    }
178    
179                                    if (indexTypeName.equals(IndexEntryStringLong.class.getName()) && !useClob) {
180                                            // User doesn't want to use CLOB handing
181                                            mapping.factory = null;
182                                    }
183    
184                                    indexMappings.add(mapping);
185    
186                                    // Populate the primary cache lookups
187                                    if (jdbcTypes == null && sqlTypes == null) {
188                                            String key = getKeyForType(typeName, null, null);
189                                            factoryByKey.put(key, mapping.factory);
190                                    }
191                                    else {
192                                            if (jdbcTypes != null) {
193                                                    StringTokenizer tok = new StringTokenizer(jdbcTypes, ",");
194                                                    while (tok.hasMoreTokens()) {
195                                                            String jdbcType = tok.nextToken();
196                                                            String key = getKeyForType(typeName, jdbcType, null);
197                                                            factoryByKey.put(key, mapping.factory);
198                                                    }
199                                            }
200                                            if (sqlTypes != null) {
201                                                    StringTokenizer tok = new StringTokenizer(sqlTypes, ",");
202                                                    while (tok.hasMoreTokens()) {
203                                                            String sqlType = tok.nextToken();
204                                                            String key = getKeyForType(typeName, null, sqlType);
205                                                            factoryByKey.put(key, mapping.factory);
206                                                    }
207                                            }
208                                    }
209                            }
210                    }
211            }
212    
213            /**
214             * Get the appropriate {@link IndexEntryFactory} subclass instance for the given {@link FieldMeta}.
215             * @param executionContext the context.
216             * @param fieldMeta either a {@link FieldMeta} for a {@link FieldMetaRole#primary primary} field or a sub-<code>FieldMeta</code>,
217             * if a <code>Collection</code> element, a <code>Map</code> key, a <code>Map</code> value or similar are indexed.
218             * @param throwExceptionIfNotFound throw an exception instead of returning <code>null</code>, if there is no {@link IndexEntryFactory} for
219             * the given <code>fieldMeta</code>.
220             * @return the appropriate {@link IndexEntryFactory} or <code>null</code>, if none is registered and <code>throwExceptionIfNotFound == false</code>.
221             */
222            public IndexEntryFactory getIndexEntryFactory(ExecutionContext executionContext, FieldMeta fieldMeta, boolean throwExceptionIfNotFound)
223            {
224                    ClassLoaderResolver clr = executionContext.getClassLoaderResolver();
225                    AbstractMemberMetaData mmd = fieldMeta.getDataNucleusMemberMetaData(executionContext);
226                    Class<?> fieldType = null;
227                    switch (fieldMeta.getRole()) {
228                            case primary:
229                                    fieldType = mmd.getType();
230                                    break;
231                            case collectionElement: {
232                                    CollectionMetaData cmd = mmd.getCollection();
233                                    if (cmd != null) {
234                                            // Even though the documentation of CollectionMetaData.getElementType() says there could be a comma-separated
235                                            // list of class names, the whole DataNucleus code-base currently ignores this possibility.
236                                            // To verify, I just tried the following field annotation:
237                                            // @Join
238                                            // @Element(types={String.class, Long.class})
239                                            // private Set<Object> set = new HashSet<Object>();
240                                            //
241                                            // The result was that DataNucleus ignored the String.class and only took the Long.class into account - cmd.getElementType()
242                                            // contained only "java.lang.Long" here. Since it would make our indexing much more complicated and we cannot test it anyway
243                                            // as long as DN does not support it, we ignore this situation for now.
244                                            // We can still implement it later (major refactoring, though), if DN ever supports it one day.
245                                            // Marco ;-)
246                                            fieldType = clr.classForName(cmd.getElementType());
247                                    }
248                            }
249                            break;
250                            case arrayElement:{
251                                    ArrayMetaData amd = mmd.getArray();
252                                    if(amd != null){
253                                            fieldType = clr.classForName(amd.getElementType());
254                                    }
255                            }
256                            break;
257                            case mapKey: {
258                                    MapMetaData mapMetaData = mmd.getMap();
259                                    if (mapMetaData != null) {
260                                            // Here, the same applies as for the CollectionMetaData.getElementType(). Marco ;-)
261                                            fieldType = clr.classForName(mapMetaData.getKeyType());
262                                    }
263                            }
264                            break;
265                            case mapValue: {
266                                    MapMetaData mapMetaData = mmd.getMap();
267                                    if (mapMetaData != null) {
268                                            // Here, the same applies as for the CollectionMetaData.getElementType(). Marco ;-)
269                                            fieldType = clr.classForName(mapMetaData.getValueType());
270                                    }
271                            }
272                            break;
273                    }
274    
275                    String jdbcType = null;
276                    String sqlType = null;
277                    if (mmd.getColumnMetaData() != null && mmd.getColumnMetaData().length > 0) {
278                            jdbcType = mmd.getColumnMetaData()[0].getJdbcType();
279                            sqlType = mmd.getColumnMetaData()[0].getSqlType();
280                    }
281                    String key = getKeyForType(fieldType.getName(), jdbcType, sqlType);
282    
283                    // Check the cache
284                    if (factoryByKey.containsKey(key)) {
285                            return factoryByKey.get(key);
286                    }
287    
288                    Iterator<IndexMapping> mappingIter = indexMappings.iterator();
289                    while (mappingIter.hasNext()) {
290                            IndexMapping mapping = mappingIter.next();
291                            if (mapping.matches(fieldType, jdbcType, sqlType)) {
292                                    factoryByKey.put(key, mapping.factory);
293                                    return mapping.factory;
294                            }
295                    }
296    
297                    if (throwExceptionIfNotFound)
298                            throw new UnsupportedDataTypeException("No IndexEntryFactory registered for this type: " + mmd);
299    
300                    factoryByKey.put(key, null);
301                    return null;
302            }
303    
304            private String getKeyForType(String javaTypeName, String jdbcTypeName, String sqlTypeName) {
305                    return javaTypeName + ":" + (jdbcTypeName != null ? jdbcTypeName : "") + ":" + (sqlTypeName != null ? sqlTypeName : "");
306            }
307    
308            /**
309             * Get the special {@link IndexEntryFactory} used for container-sizes. This special index
310             * allows using {@link Collection#isEmpty()}, {@link Collection#size()} and the like in JDOQL
311             * (or "SIZE(...)" and the like in JPQL).
312             * @return the special {@link IndexEntryFactory} used for container-sizes.
313             */
314            public IndexEntryFactory getIndexEntryFactoryForContainerSize() {
315                    return indexEntryFactoryContainerSize;
316            }
317    
318            public Set<Class<? extends IndexEntry>> getIndexEntryClasses() {
319                    Set<Class<? extends IndexEntry>> result = new HashSet<Class<? extends IndexEntry>>();
320                    Class<? extends IndexEntry> indexEntryClass = getIndexEntryFactoryForContainerSize().getIndexEntryClass();
321                    if (indexEntryClass == null)
322                            throw new IllegalStateException("indexEntryClass == null");
323    
324                    result.add(indexEntryClass);
325    
326                    for (IndexMapping indexMapping : indexMappings) {
327                            if (indexMapping.factory == null) // indexing for this type explicitely disabled => no factory.
328                                    continue;
329    
330                            indexEntryClass = indexMapping.factory.getIndexEntryClass();
331                            if (indexEntryClass == null)
332                                    throw new IllegalStateException("indexEntryClass == null");
333    
334                            result.add(indexEntryClass);
335                    }
336                    return result;
337            }
338    }