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 }