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 }