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 }