001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.connector.jbosscache;
023    
024    import java.io.ByteArrayInputStream;
025    import java.io.ByteArrayOutputStream;
026    import java.io.IOException;
027    import java.io.ObjectInputStream;
028    import java.io.ObjectOutputStream;
029    import java.util.Enumeration;
030    import java.util.HashMap;
031    import java.util.Hashtable;
032    import java.util.Map;
033    import java.util.UUID;
034    import java.util.concurrent.atomic.AtomicInteger;
035    import javax.naming.BinaryRefAddr;
036    import javax.naming.Context;
037    import javax.naming.InitialContext;
038    import javax.naming.RefAddr;
039    import javax.naming.Reference;
040    import javax.naming.Referenceable;
041    import javax.naming.StringRefAddr;
042    import javax.naming.spi.ObjectFactory;
043    import net.jcip.annotations.ThreadSafe;
044    import org.jboss.cache.Cache;
045    import org.jboss.cache.CacheFactory;
046    import org.jboss.cache.DefaultCacheFactory;
047    import org.jboss.dna.common.i18n.I18n;
048    import org.jboss.dna.graph.DnaLexicon;
049    import org.jboss.dna.graph.cache.CachePolicy;
050    import org.jboss.dna.graph.connectors.RepositoryConnection;
051    import org.jboss.dna.graph.connectors.RepositoryContext;
052    import org.jboss.dna.graph.connectors.RepositorySource;
053    import org.jboss.dna.graph.connectors.RepositorySourceCapabilities;
054    import org.jboss.dna.graph.connectors.RepositorySourceException;
055    import org.jboss.dna.graph.properties.Name;
056    import org.jboss.dna.graph.properties.Property;
057    
058    /**
059     * A repository source that uses a JBoss Cache instance to manage the content. This source is capable of using an existing
060     * {@link Cache} instance or creating a new instance. This process is controlled entirely by the JavaBean properties of the
061     * JBossCacheSource instance.
062     * <p>
063     * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it attempts to
064     * create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
065     * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache configuration
066     * name} if supplied or the default configuration if not set.
067     * </p>
068     * <p>
069     * Like other {@link RepositorySource} classes, instances of JBossCacheSource can be placed into JNDI and do support the creation
070     * of {@link Referenceable JNDI referenceable} objects and resolution of references into JBossCacheSource.
071     * </p>
072     * 
073     * @author Randall Hauch
074     */
075    @ThreadSafe
076    public class JBossCacheSource implements RepositorySource, ObjectFactory {
077    
078        private static final long serialVersionUID = 1L;
079        /**
080         * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
081         */
082        public static final int DEFAULT_RETRY_LIMIT = 0;
083        public static final String DEFAULT_UUID_PROPERTY_NAME = DnaLexicon.UUID.getString();
084    
085        protected static final String ROOT_NODE_UUID = "rootNodeUuid";
086        protected static final String SOURCE_NAME = "sourceName";
087        protected static final String DEFAULT_CACHE_POLICY = "defaultCachePolicy";
088        protected static final String CACHE_CONFIGURATION_NAME = "cacheConfigurationName";
089        protected static final String CACHE_FACTORY_JNDI_NAME = "cacheFactoryJndiName";
090        protected static final String CACHE_JNDI_NAME = "cacheJndiName";
091        protected static final String UUID_PROPERTY_NAME = "uuidPropertyName";
092        protected static final String RETRY_LIMIT = "retryLimit";
093    
094        private String name;
095        private UUID rootNodeUuid = UUID.randomUUID();
096        private CachePolicy defaultCachePolicy;
097        private String cacheConfigurationName;
098        private String cacheFactoryJndiName;
099        private String cacheJndiName;
100        private String uuidPropertyName = DEFAULT_UUID_PROPERTY_NAME;
101        private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
102        private transient Cache<Name, Object> cache;
103        private transient Context jndiContext;
104        private transient RepositoryContext repositoryContext;
105    
106        /**
107         * Create a repository source instance.
108         */
109        public JBossCacheSource() {
110        }
111    
112        /**
113         * {@inheritDoc}
114         * 
115         * @see org.jboss.dna.graph.connectors.RepositorySource#initialize(org.jboss.dna.graph.connectors.RepositoryContext)
116         */
117        public void initialize( RepositoryContext context ) throws RepositorySourceException {
118            this.repositoryContext = context;
119        }
120    
121        /**
122         * @return repositoryContext
123         */
124        public RepositoryContext getRepositoryContext() {
125            return repositoryContext;
126        }
127    
128        /**
129         * {@inheritDoc}
130         */
131        public String getName() {
132            return this.name;
133        }
134    
135        /**
136         * {@inheritDoc}
137         * 
138         * @see org.jboss.dna.graph.connectors.RepositorySource#getRetryLimit()
139         */
140        public int getRetryLimit() {
141            return retryLimit.get();
142        }
143    
144        /**
145         * {@inheritDoc}
146         * 
147         * @see org.jboss.dna.graph.connectors.RepositorySource#setRetryLimit(int)
148         */
149        public void setRetryLimit( int limit ) {
150            retryLimit.set(limit < 0 ? 0 : limit);
151        }
152    
153        /**
154         * Set the name of this source
155         * 
156         * @param name the name for this source
157         */
158        public synchronized void setName( String name ) {
159            if (this.name == name || this.name != null && this.name.equals(name)) return; // unchanged
160            this.name = name;
161        }
162    
163        /**
164         * Get the default cache policy for this source, or null if the global default cache policy should be used
165         * 
166         * @return the default cache policy, or null if this source has no explicit default cache policy
167         */
168        public CachePolicy getDefaultCachePolicy() {
169            return defaultCachePolicy;
170        }
171    
172        /**
173         * @param defaultCachePolicy Sets defaultCachePolicy to the specified value.
174         */
175        public synchronized void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) {
176            if (this.defaultCachePolicy == defaultCachePolicy || this.defaultCachePolicy != null
177                && this.defaultCachePolicy.equals(defaultCachePolicy)) return; // unchanged
178            this.defaultCachePolicy = defaultCachePolicy;
179        }
180    
181        /**
182         * Get the name in JNDI of a {@link Cache} instance that should be used by this source.
183         * <p>
184         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
185         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
186         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
187         * configuration name} if supplied or the default configuration if not set.
188         * </p>
189         * 
190         * @return the JNDI name of the {@link Cache} instance that should be used, or null if the cache is to be created with a cache
191         *         factory {@link #getCacheFactoryJndiName() found in JNDI} using the specified {@link #getCacheConfigurationName()
192         *         cache configuration name}.
193         * @see #setCacheJndiName(String)
194         * @see #getCacheConfigurationName()
195         * @see #getCacheFactoryJndiName()
196         */
197        public String getCacheJndiName() {
198            return cacheJndiName;
199        }
200    
201        /**
202         * Set the name in JNDI of a {@link Cache} instance that should be used by this source.
203         * <p>
204         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
205         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
206         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
207         * configuration name} if supplied or the default configuration if not set.
208         * </p>
209         * 
210         * @param cacheJndiName the JNDI name of the {@link Cache} instance that should be used, or null if the cache is to be created
211         *        with a cache factory {@link #getCacheFactoryJndiName() found in JNDI} using the specified
212         *        {@link #getCacheConfigurationName() cache configuration name}.
213         * @see #getCacheJndiName()
214         * @see #getCacheConfigurationName()
215         * @see #getCacheFactoryJndiName()
216         */
217        public synchronized void setCacheJndiName( String cacheJndiName ) {
218            if (this.cacheJndiName == cacheJndiName || this.cacheJndiName != null && this.cacheJndiName.equals(cacheJndiName)) return; // unchanged
219            this.cacheJndiName = cacheJndiName;
220        }
221    
222        /**
223         * Get the name in JNDI of a {@link CacheFactory} instance that should be used to create the cache for this source.
224         * <p>
225         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
226         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
227         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
228         * configuration name} if supplied or the default configuration if not set.
229         * </p>
230         * 
231         * @return the JNDI name of the {@link CacheFactory} instance that should be used, or null if the {@link DefaultCacheFactory}
232         *         should be used if a cache is to be created
233         * @see #setCacheFactoryJndiName(String)
234         * @see #getCacheConfigurationName()
235         * @see #getCacheJndiName()
236         */
237        public String getCacheFactoryJndiName() {
238            return cacheFactoryJndiName;
239        }
240    
241        /**
242         * Set the name in JNDI of a {@link CacheFactory} instance that should be used to obtain the {@link Cache} instance used by
243         * this source.
244         * <p>
245         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
246         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
247         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
248         * configuration name} if supplied or the default configuration if not set.
249         * </p>
250         * 
251         * @param jndiName the JNDI name of the {@link CacheFactory} instance that should be used, or null if the
252         *        {@link DefaultCacheFactory} should be used if a cache is to be created
253         * @see #setCacheFactoryJndiName(String)
254         * @see #getCacheConfigurationName()
255         * @see #getCacheJndiName()
256         */
257        public synchronized void setCacheFactoryJndiName( String jndiName ) {
258            if (this.cacheFactoryJndiName == jndiName || this.cacheFactoryJndiName != null
259                && this.cacheFactoryJndiName.equals(jndiName)) return; // unchanged
260            this.cacheFactoryJndiName = jndiName;
261        }
262    
263        /**
264         * Get the name of the configuration that should be used if a {@link Cache cache} is to be created using the
265         * {@link CacheFactory} found in JNDI or the {@link DefaultCacheFactory} if needed.
266         * <p>
267         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
268         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
269         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
270         * configuration name} if supplied or the default configuration if not set.
271         * </p>
272         * 
273         * @return the name of the configuration that should be passed to the {@link CacheFactory}, or null if the default
274         *         configuration should be used
275         * @see #setCacheConfigurationName(String)
276         * @see #getCacheFactoryJndiName()
277         * @see #getCacheJndiName()
278         */
279        public String getCacheConfigurationName() {
280            return cacheConfigurationName;
281        }
282    
283        /**
284         * Get the name of the configuration that should be used if a {@link Cache cache} is to be created using the
285         * {@link CacheFactory} found in JNDI or the {@link DefaultCacheFactory} if needed.
286         * <p>
287         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
288         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
289         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
290         * configuration name} if supplied or the default configuration if not set.
291         * </p>
292         * 
293         * @param cacheConfigurationName the name of the configuration that should be passed to the {@link CacheFactory}, or null if
294         *        the default configuration should be used
295         * @see #getCacheConfigurationName()
296         * @see #getCacheFactoryJndiName()
297         * @see #getCacheJndiName()
298         */
299        public synchronized void setCacheConfigurationName( String cacheConfigurationName ) {
300            if (this.cacheConfigurationName == cacheConfigurationName || this.cacheConfigurationName != null
301                && this.cacheConfigurationName.equals(cacheConfigurationName)) return; // unchanged
302            this.cacheConfigurationName = cacheConfigurationName;
303        }
304    
305        /**
306         * Get the UUID of the root node for the cache. If the cache exists, this UUID is not used but is instead set to the UUID of
307         * the existing root node.
308         * 
309         * @return the UUID of the root node for the cache.
310         */
311        public String getRootNodeUuid() {
312            return this.rootNodeUuid.toString();
313        }
314    
315        /**
316         * Get the UUID of the root node for the cache. If the cache exists, this UUID is not used but is instead set to the UUID of
317         * the existing root node.
318         * 
319         * @return the UUID of the root node for the cache.
320         */
321        public UUID getRootNodeUuidObject() {
322            return this.rootNodeUuid;
323        }
324    
325        /**
326         * Set the UUID of the root node in this repository. If the cache exists, this UUID is not used but is instead set to the UUID
327         * of the existing root node.
328         * 
329         * @param rootNodeUuid the UUID of the root node for the cache, or null if the UUID should be randomly generated
330         */
331        public synchronized void setRootNodeUuid( String rootNodeUuid ) {
332            UUID uuid = null;
333            if (rootNodeUuid == null) uuid = UUID.randomUUID();
334            else uuid = UUID.fromString(rootNodeUuid);
335            if (this.rootNodeUuid.equals(uuid)) return; // unchanged
336            this.rootNodeUuid = uuid;
337        }
338    
339        /**
340         * Get the {@link Property#getName() property name} where the UUID is stored for each node.
341         * 
342         * @return the name of the UUID property; never null
343         */
344        public String getUuidPropertyName() {
345            return this.uuidPropertyName;
346        }
347    
348        /**
349         * Set the {@link Property#getName() property name} where the UUID is stored for each node.
350         * 
351         * @param uuidPropertyName the name of the UUID property, or null if the {@link #DEFAULT_UUID_PROPERTY_NAME default name}
352         *        should be used
353         */
354        public synchronized void setUuidPropertyName( String uuidPropertyName ) {
355            if (uuidPropertyName == null || uuidPropertyName.trim().length() == 0) uuidPropertyName = DEFAULT_UUID_PROPERTY_NAME;
356            if (this.uuidPropertyName.equals(uuidPropertyName)) return; // unchanged
357            this.uuidPropertyName = uuidPropertyName;
358        }
359    
360        /**
361         * {@inheritDoc}
362         * 
363         * @see org.jboss.dna.graph.connectors.RepositorySource#getConnection()
364         */
365        @SuppressWarnings( "unchecked" )
366        public RepositoryConnection getConnection() throws RepositorySourceException {
367            if (getName() == null) {
368                I18n msg = JBossCacheConnectorI18n.propertyIsRequired;
369                throw new RepositorySourceException(getName(), msg.text("name"));
370            }
371            if (getUuidPropertyName() == null) {
372                I18n msg = JBossCacheConnectorI18n.propertyIsRequired;
373                throw new RepositorySourceException(getName(), msg.text("uuidPropertyName"));
374            }
375            if (this.cache == null) {
376                // First look for an existing cache instance in JNDI ...
377                Context context = getContext();
378                String jndiName = this.getCacheJndiName();
379                if (jndiName != null && jndiName.trim().length() != 0) {
380                    Object object = null;
381                    try {
382                        if (context == null) context = new InitialContext();
383                        object = context.lookup(jndiName);
384                        if (object != null) cache = (Cache<Name, Object>)object;
385                    } catch (ClassCastException err) {
386                        I18n msg = JBossCacheConnectorI18n.objectFoundInJndiWasNotCache;
387                        String className = object != null ? object.getClass().getName() : "null";
388                        throw new RepositorySourceException(getName(), msg.text(jndiName, this.getName(), className), err);
389                    } catch (Throwable err) {
390                        // try loading
391                    }
392                }
393                if (cache == null) {
394                    // Then look for a cache factory in JNDI ...
395                    CacheFactory<Name, Object> cacheFactory = null;
396                    jndiName = getCacheFactoryJndiName();
397                    if (jndiName != null && jndiName.trim().length() != 0) {
398                        Object object = null;
399                        try {
400                            if (context == null) context = new InitialContext();
401                            object = context.lookup(jndiName);
402                            if (object != null) cacheFactory = (CacheFactory<Name, Object>)object;
403                        } catch (ClassCastException err) {
404                            I18n msg = JBossCacheConnectorI18n.objectFoundInJndiWasNotCacheFactory;
405                            String className = object != null ? object.getClass().getName() : "null";
406                            throw new RepositorySourceException(getName(), msg.text(jndiName, this.getName(), className), err);
407                        } catch (Throwable err) {
408                            // try loading
409                        }
410                    }
411                    if (cacheFactory == null) cacheFactory = new DefaultCacheFactory<Name, Object>();
412    
413                    // Now, get the configuration name ...
414                    String configName = this.getCacheConfigurationName();
415                    if (configName != null) {
416                        cache = cacheFactory.createCache(configName);
417                    } else {
418                        cache = cacheFactory.createCache();
419                    }
420                }
421            }
422            return new JBossCacheConnection(this, this.cache);
423        }
424    
425        protected Context getContext() {
426            return this.jndiContext;
427        }
428    
429        protected synchronized void setContext( Context context ) {
430            this.jndiContext = context;
431        }
432    
433        /**
434         * {@inheritDoc}
435         */
436        @Override
437        public boolean equals( Object obj ) {
438            if (obj == this) return true;
439            if (obj instanceof JBossCacheSource) {
440                JBossCacheSource that = (JBossCacheSource)obj;
441                if (this.getName() == null) {
442                    if (that.getName() != null) return false;
443                } else {
444                    if (!this.getName().equals(that.getName())) return false;
445                }
446                return true;
447            }
448            return false;
449        }
450    
451        /**
452         * {@inheritDoc}
453         */
454        public synchronized Reference getReference() {
455            String className = getClass().getName();
456            String factoryClassName = this.getClass().getName();
457            Reference ref = new Reference(className, factoryClassName, null);
458    
459            if (getName() != null) {
460                ref.add(new StringRefAddr(SOURCE_NAME, getName()));
461            }
462            if (getRootNodeUuid() != null) {
463                ref.add(new StringRefAddr(ROOT_NODE_UUID, getRootNodeUuid().toString()));
464            }
465            if (getUuidPropertyName() != null) {
466                ref.add(new StringRefAddr(UUID_PROPERTY_NAME, getUuidPropertyName()));
467            }
468            if (getCacheJndiName() != null) {
469                ref.add(new StringRefAddr(CACHE_JNDI_NAME, getCacheJndiName()));
470            }
471            if (getCacheFactoryJndiName() != null) {
472                ref.add(new StringRefAddr(CACHE_FACTORY_JNDI_NAME, getCacheFactoryJndiName()));
473            }
474            if (getCacheConfigurationName() != null) {
475                ref.add(new StringRefAddr(CACHE_CONFIGURATION_NAME, getCacheConfigurationName()));
476            }
477            if (getDefaultCachePolicy() != null) {
478                ByteArrayOutputStream baos = new ByteArrayOutputStream();
479                CachePolicy policy = getDefaultCachePolicy();
480                try {
481                    ObjectOutputStream oos = new ObjectOutputStream(baos);
482                    oos.writeObject(policy);
483                    ref.add(new BinaryRefAddr(DEFAULT_CACHE_POLICY, baos.toByteArray()));
484                } catch (IOException e) {
485                    I18n msg = JBossCacheConnectorI18n.errorSerializingCachePolicyInSource;
486                    throw new RepositorySourceException(getName(), msg.text(policy.getClass().getName(), getName()), e);
487                }
488            }
489            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
490            return ref;
491        }
492    
493        /**
494         * {@inheritDoc}
495         */
496        public Object getObjectInstance( Object obj,
497                                         javax.naming.Name name,
498                                         Context nameCtx,
499                                         Hashtable<?, ?> environment ) throws Exception {
500            if (obj instanceof Reference) {
501                Map<String, Object> values = new HashMap<String, Object>();
502                Reference ref = (Reference)obj;
503                Enumeration<?> en = ref.getAll();
504                while (en.hasMoreElements()) {
505                    RefAddr subref = (RefAddr)en.nextElement();
506                    if (subref instanceof StringRefAddr) {
507                        String key = subref.getType();
508                        Object value = subref.getContent();
509                        if (value != null) values.put(key, value.toString());
510                    } else if (subref instanceof BinaryRefAddr) {
511                        String key = subref.getType();
512                        Object value = subref.getContent();
513                        if (value instanceof byte[]) {
514                            // Deserialize ...
515                            ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value);
516                            ObjectInputStream ois = new ObjectInputStream(bais);
517                            value = ois.readObject();
518                            values.put(key, value);
519                        }
520                    }
521                }
522                String sourceName = (String)values.get(SOURCE_NAME);
523                String rootNodeUuidString = (String)values.get(ROOT_NODE_UUID);
524                String uuidPropertyName = (String)values.get(UUID_PROPERTY_NAME);
525                String cacheJndiName = (String)values.get(CACHE_JNDI_NAME);
526                String cacheFactoryJndiName = (String)values.get(CACHE_FACTORY_JNDI_NAME);
527                String cacheConfigurationName = (String)values.get(CACHE_CONFIGURATION_NAME);
528                Object defaultCachePolicy = values.get(DEFAULT_CACHE_POLICY);
529                String retryLimit = (String)values.get(RETRY_LIMIT);
530    
531                // Create the source instance ...
532                JBossCacheSource source = new JBossCacheSource();
533                if (sourceName != null) source.setName(sourceName);
534                if (rootNodeUuidString != null) source.setRootNodeUuid(rootNodeUuidString);
535                if (uuidPropertyName != null) source.setUuidPropertyName(uuidPropertyName);
536                if (cacheJndiName != null) source.setCacheJndiName(cacheJndiName);
537                if (cacheFactoryJndiName != null) source.setCacheFactoryJndiName(cacheFactoryJndiName);
538                if (cacheConfigurationName != null) source.setCacheConfigurationName(cacheConfigurationName);
539                if (defaultCachePolicy instanceof CachePolicy) {
540                    source.setDefaultCachePolicy((CachePolicy)defaultCachePolicy);
541                }
542                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
543                return source;
544            }
545            return null;
546        }
547    
548        /**
549         * {@inheritDoc}
550         * 
551         * @see org.jboss.dna.graph.connectors.RepositorySource#getCapabilities()
552         */
553        public RepositorySourceCapabilities getCapabilities() {
554            return new Capabilities();
555        }
556    
557        protected class Capabilities implements RepositorySourceCapabilities {
558            public boolean supportsSameNameSiblings() {
559                return true;
560            }
561    
562            public boolean supportsUpdates() {
563                return true;
564            }
565        }
566    }