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 }