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