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 RepositorySourceCapabilities CAPABILITIES = new RepositorySourceCapabilities(true, true);
086    
087        protected static final String ROOT_NODE_UUID = "rootNodeUuid";
088        protected static final String SOURCE_NAME = "sourceName";
089        protected static final String DEFAULT_CACHE_POLICY = "defaultCachePolicy";
090        protected static final String CACHE_CONFIGURATION_NAME = "cacheConfigurationName";
091        protected static final String CACHE_FACTORY_JNDI_NAME = "cacheFactoryJndiName";
092        protected static final String CACHE_JNDI_NAME = "cacheJndiName";
093        protected static final String UUID_PROPERTY_NAME = "uuidPropertyName";
094        protected static final String RETRY_LIMIT = "retryLimit";
095    
096        private String name;
097        private UUID rootNodeUuid = UUID.randomUUID();
098        private CachePolicy defaultCachePolicy;
099        private String cacheConfigurationName;
100        private String cacheFactoryJndiName;
101        private String cacheJndiName;
102        private String uuidPropertyName = DEFAULT_UUID_PROPERTY_NAME;
103        private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
104        private transient Cache<Name, Object> cache;
105        private transient Context jndiContext;
106        private transient RepositoryContext repositoryContext;
107    
108        /**
109         * Create a repository source instance.
110         */
111        public JBossCacheSource() {
112        }
113    
114        /**
115         * {@inheritDoc}
116         * 
117         * @see org.jboss.dna.graph.connectors.RepositorySource#initialize(org.jboss.dna.graph.connectors.RepositoryContext)
118         */
119        public void initialize( RepositoryContext context ) throws RepositorySourceException {
120            this.repositoryContext = context;
121        }
122    
123        /**
124         * @return repositoryContext
125         */
126        public RepositoryContext getRepositoryContext() {
127            return repositoryContext;
128        }
129    
130        /**
131         * {@inheritDoc}
132         */
133        public String getName() {
134            return this.name;
135        }
136    
137        /**
138         * {@inheritDoc}
139         * 
140         * @see org.jboss.dna.graph.connectors.RepositorySource#getRetryLimit()
141         */
142        public int getRetryLimit() {
143            return retryLimit.get();
144        }
145    
146        /**
147         * {@inheritDoc}
148         * 
149         * @see org.jboss.dna.graph.connectors.RepositorySource#setRetryLimit(int)
150         */
151        public void setRetryLimit( int limit ) {
152            retryLimit.set(limit < 0 ? 0 : limit);
153        }
154    
155        /**
156         * Set the name of this source
157         * 
158         * @param name the name for this source
159         */
160        public synchronized void setName( String name ) {
161            if (this.name == name || this.name != null && this.name.equals(name)) return; // unchanged
162            this.name = name;
163        }
164    
165        /**
166         * Get the default cache policy for this source, or null if the global default cache policy should be used
167         * 
168         * @return the default cache policy, or null if this source has no explicit default cache policy
169         */
170        public CachePolicy getDefaultCachePolicy() {
171            return defaultCachePolicy;
172        }
173    
174        /**
175         * @param defaultCachePolicy Sets defaultCachePolicy to the specified value.
176         */
177        public synchronized void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) {
178            if (this.defaultCachePolicy == defaultCachePolicy || this.defaultCachePolicy != null
179                && this.defaultCachePolicy.equals(defaultCachePolicy)) return; // unchanged
180            this.defaultCachePolicy = defaultCachePolicy;
181        }
182    
183        /**
184         * Get the name in JNDI of a {@link Cache} instance that should be used by this source.
185         * <p>
186         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
187         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
188         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
189         * configuration name} if supplied or the default configuration if not set.
190         * </p>
191         * 
192         * @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
193         *         factory {@link #getCacheFactoryJndiName() found in JNDI} using the specified {@link #getCacheConfigurationName()
194         *         cache configuration name}.
195         * @see #setCacheJndiName(String)
196         * @see #getCacheConfigurationName()
197         * @see #getCacheFactoryJndiName()
198         */
199        public String getCacheJndiName() {
200            return cacheJndiName;
201        }
202    
203        /**
204         * Set the name in JNDI of a {@link Cache} instance that should be used by this source.
205         * <p>
206         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
207         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
208         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
209         * configuration name} if supplied or the default configuration if not set.
210         * </p>
211         * 
212         * @param cacheJndiName the JNDI name of the {@link Cache} instance that should be used, or null if the cache is to be created
213         *        with a cache factory {@link #getCacheFactoryJndiName() found in JNDI} using the specified
214         *        {@link #getCacheConfigurationName() cache configuration name}.
215         * @see #getCacheJndiName()
216         * @see #getCacheConfigurationName()
217         * @see #getCacheFactoryJndiName()
218         */
219        public synchronized void setCacheJndiName( String cacheJndiName ) {
220            if (this.cacheJndiName == cacheJndiName || this.cacheJndiName != null && this.cacheJndiName.equals(cacheJndiName)) return; // unchanged
221            this.cacheJndiName = cacheJndiName;
222        }
223    
224        /**
225         * Get the name in JNDI of a {@link CacheFactory} instance that should be used to create the cache for this source.
226         * <p>
227         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
228         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
229         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
230         * configuration name} if supplied or the default configuration if not set.
231         * </p>
232         * 
233         * @return the JNDI name of the {@link CacheFactory} instance that should be used, or null if the {@link DefaultCacheFactory}
234         *         should be used if a cache is to be created
235         * @see #setCacheFactoryJndiName(String)
236         * @see #getCacheConfigurationName()
237         * @see #getCacheJndiName()
238         */
239        public String getCacheFactoryJndiName() {
240            return cacheFactoryJndiName;
241        }
242    
243        /**
244         * Set the name in JNDI of a {@link CacheFactory} instance that should be used to obtain the {@link Cache} instance used by
245         * this source.
246         * <p>
247         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
248         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
249         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
250         * configuration name} if supplied or the default configuration if not set.
251         * </p>
252         * 
253         * @param jndiName the JNDI name of the {@link CacheFactory} instance that should be used, or null if the
254         *        {@link DefaultCacheFactory} should be used if a cache is to be created
255         * @see #setCacheFactoryJndiName(String)
256         * @see #getCacheConfigurationName()
257         * @see #getCacheJndiName()
258         */
259        public synchronized void setCacheFactoryJndiName( String jndiName ) {
260            if (this.cacheFactoryJndiName == jndiName || this.cacheFactoryJndiName != null
261                && this.cacheFactoryJndiName.equals(jndiName)) return; // unchanged
262            this.cacheFactoryJndiName = jndiName;
263        }
264    
265        /**
266         * Get the name of the configuration that should be used if a {@link Cache cache} is to be created using the
267         * {@link CacheFactory} found in JNDI or the {@link DefaultCacheFactory} if needed.
268         * <p>
269         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
270         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
271         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
272         * configuration name} if supplied or the default configuration if not set.
273         * </p>
274         * 
275         * @return the name of the configuration that should be passed to the {@link CacheFactory}, or null if the default
276         *         configuration should be used
277         * @see #setCacheConfigurationName(String)
278         * @see #getCacheFactoryJndiName()
279         * @see #getCacheJndiName()
280         */
281        public String getCacheConfigurationName() {
282            return cacheConfigurationName;
283        }
284    
285        /**
286         * Get the name of the configuration that should be used if a {@link Cache cache} is to be created using the
287         * {@link CacheFactory} found in JNDI or the {@link DefaultCacheFactory} if needed.
288         * <p>
289         * This source first attempts to find an existing cache in {@link #getCacheJndiName() JNDI}. If none is found, then it
290         * attempts to create a cache instance using the {@link CacheFactory} found in {@link #getCacheFactoryJndiName() JNDI} (or the
291         * {@link DefaultCacheFactory} if no such factory is available) and the {@link #getCacheConfigurationName() cache
292         * configuration name} if supplied or the default configuration if not set.
293         * </p>
294         * 
295         * @param cacheConfigurationName the name of the configuration that should be passed to the {@link CacheFactory}, or null if
296         *        the default configuration should be used
297         * @see #getCacheConfigurationName()
298         * @see #getCacheFactoryJndiName()
299         * @see #getCacheJndiName()
300         */
301        public synchronized void setCacheConfigurationName( String cacheConfigurationName ) {
302            if (this.cacheConfigurationName == cacheConfigurationName || this.cacheConfigurationName != null
303                && this.cacheConfigurationName.equals(cacheConfigurationName)) return; // unchanged
304            this.cacheConfigurationName = cacheConfigurationName;
305        }
306    
307        /**
308         * 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
309         * the existing root node.
310         * 
311         * @return the UUID of the root node for the cache.
312         */
313        public String getRootNodeUuid() {
314            return this.rootNodeUuid.toString();
315        }
316    
317        /**
318         * 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
319         * the existing root node.
320         * 
321         * @return the UUID of the root node for the cache.
322         */
323        public UUID getRootNodeUuidObject() {
324            return this.rootNodeUuid;
325        }
326    
327        /**
328         * 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
329         * of the existing root node.
330         * 
331         * @param rootNodeUuid the UUID of the root node for the cache, or null if the UUID should be randomly generated
332         */
333        public synchronized void setRootNodeUuid( String rootNodeUuid ) {
334            UUID uuid = null;
335            if (rootNodeUuid == null) uuid = UUID.randomUUID();
336            else uuid = UUID.fromString(rootNodeUuid);
337            if (this.rootNodeUuid.equals(uuid)) return; // unchanged
338            this.rootNodeUuid = uuid;
339        }
340    
341        /**
342         * Get the {@link Property#getName() property name} where the UUID is stored for each node.
343         * 
344         * @return the name of the UUID property; never null
345         */
346        public String getUuidPropertyName() {
347            return this.uuidPropertyName;
348        }
349    
350        /**
351         * Set the {@link Property#getName() property name} where the UUID is stored for each node.
352         * 
353         * @param uuidPropertyName the name of the UUID property, or null if the {@link #DEFAULT_UUID_PROPERTY_NAME default name}
354         *        should be used
355         */
356        public synchronized void setUuidPropertyName( String uuidPropertyName ) {
357            if (uuidPropertyName == null || uuidPropertyName.trim().length() == 0) uuidPropertyName = DEFAULT_UUID_PROPERTY_NAME;
358            if (this.uuidPropertyName.equals(uuidPropertyName)) return; // unchanged
359            this.uuidPropertyName = uuidPropertyName;
360        }
361    
362        /**
363         * {@inheritDoc}
364         * 
365         * @see org.jboss.dna.graph.connectors.RepositorySource#getConnection()
366         */
367        @SuppressWarnings( "unchecked" )
368        public RepositoryConnection getConnection() throws RepositorySourceException {
369            if (getName() == null) {
370                I18n msg = JBossCacheConnectorI18n.propertyIsRequired;
371                throw new RepositorySourceException(getName(), msg.text("name"));
372            }
373            if (getUuidPropertyName() == null) {
374                I18n msg = JBossCacheConnectorI18n.propertyIsRequired;
375                throw new RepositorySourceException(getName(), msg.text("uuidPropertyName"));
376            }
377            if (this.cache == null) {
378                // First look for an existing cache instance in JNDI ...
379                Context context = getContext();
380                String jndiName = this.getCacheJndiName();
381                if (jndiName != null && jndiName.trim().length() != 0) {
382                    Object object = null;
383                    try {
384                        if (context == null) context = new InitialContext();
385                        object = context.lookup(jndiName);
386                        if (object != null) cache = (Cache<Name, Object>)object;
387                    } catch (ClassCastException err) {
388                        I18n msg = JBossCacheConnectorI18n.objectFoundInJndiWasNotCache;
389                        String className = object != null ? object.getClass().getName() : "null";
390                        throw new RepositorySourceException(getName(), msg.text(jndiName, this.getName(), className), err);
391                    } catch (Throwable err) {
392                        // try loading
393                    }
394                }
395                if (cache == null) {
396                    // Then look for a cache factory in JNDI ...
397                    CacheFactory<Name, Object> cacheFactory = null;
398                    jndiName = getCacheFactoryJndiName();
399                    if (jndiName != null && jndiName.trim().length() != 0) {
400                        Object object = null;
401                        try {
402                            if (context == null) context = new InitialContext();
403                            object = context.lookup(jndiName);
404                            if (object != null) cacheFactory = (CacheFactory<Name, Object>)object;
405                        } catch (ClassCastException err) {
406                            I18n msg = JBossCacheConnectorI18n.objectFoundInJndiWasNotCacheFactory;
407                            String className = object != null ? object.getClass().getName() : "null";
408                            throw new RepositorySourceException(getName(), msg.text(jndiName, this.getName(), className), err);
409                        } catch (Throwable err) {
410                            // try loading
411                        }
412                    }
413                    if (cacheFactory == null) cacheFactory = new DefaultCacheFactory<Name, Object>();
414    
415                    // Now, get the configuration name ...
416                    String configName = this.getCacheConfigurationName();
417                    if (configName != null) {
418                        cache = cacheFactory.createCache(configName);
419                    } else {
420                        cache = cacheFactory.createCache();
421                    }
422                }
423            }
424            return new JBossCacheConnection(this, this.cache);
425        }
426    
427        protected Context getContext() {
428            return this.jndiContext;
429        }
430    
431        protected synchronized void setContext( Context context ) {
432            this.jndiContext = context;
433        }
434    
435        /**
436         * {@inheritDoc}
437         */
438        @Override
439        public boolean equals( Object obj ) {
440            if (obj == this) return true;
441            if (obj instanceof JBossCacheSource) {
442                JBossCacheSource that = (JBossCacheSource)obj;
443                if (this.getName() == null) {
444                    if (that.getName() != null) return false;
445                } else {
446                    if (!this.getName().equals(that.getName())) return false;
447                }
448                return true;
449            }
450            return false;
451        }
452    
453        /**
454         * {@inheritDoc}
455         */
456        public synchronized Reference getReference() {
457            String className = getClass().getName();
458            String factoryClassName = this.getClass().getName();
459            Reference ref = new Reference(className, factoryClassName, null);
460    
461            if (getName() != null) {
462                ref.add(new StringRefAddr(SOURCE_NAME, getName()));
463            }
464            if (getRootNodeUuid() != null) {
465                ref.add(new StringRefAddr(ROOT_NODE_UUID, getRootNodeUuid().toString()));
466            }
467            if (getUuidPropertyName() != null) {
468                ref.add(new StringRefAddr(UUID_PROPERTY_NAME, getUuidPropertyName()));
469            }
470            if (getCacheJndiName() != null) {
471                ref.add(new StringRefAddr(CACHE_JNDI_NAME, getCacheJndiName()));
472            }
473            if (getCacheFactoryJndiName() != null) {
474                ref.add(new StringRefAddr(CACHE_FACTORY_JNDI_NAME, getCacheFactoryJndiName()));
475            }
476            if (getCacheConfigurationName() != null) {
477                ref.add(new StringRefAddr(CACHE_CONFIGURATION_NAME, getCacheConfigurationName()));
478            }
479            if (getDefaultCachePolicy() != null) {
480                ByteArrayOutputStream baos = new ByteArrayOutputStream();
481                CachePolicy policy = getDefaultCachePolicy();
482                try {
483                    ObjectOutputStream oos = new ObjectOutputStream(baos);
484                    oos.writeObject(policy);
485                    ref.add(new BinaryRefAddr(DEFAULT_CACHE_POLICY, baos.toByteArray()));
486                } catch (IOException e) {
487                    I18n msg = JBossCacheConnectorI18n.errorSerializingCachePolicyInSource;
488                    throw new RepositorySourceException(getName(), msg.text(policy.getClass().getName(), getName()), e);
489                }
490            }
491            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
492            return ref;
493        }
494    
495        /**
496         * {@inheritDoc}
497         */
498        public Object getObjectInstance( Object obj,
499                                         javax.naming.Name name,
500                                         Context nameCtx,
501                                         Hashtable<?, ?> environment ) throws Exception {
502            if (obj instanceof Reference) {
503                Map<String, Object> values = new HashMap<String, Object>();
504                Reference ref = (Reference)obj;
505                Enumeration<?> en = ref.getAll();
506                while (en.hasMoreElements()) {
507                    RefAddr subref = (RefAddr)en.nextElement();
508                    if (subref instanceof StringRefAddr) {
509                        String key = subref.getType();
510                        Object value = subref.getContent();
511                        if (value != null) values.put(key, value.toString());
512                    } else if (subref instanceof BinaryRefAddr) {
513                        String key = subref.getType();
514                        Object value = subref.getContent();
515                        if (value instanceof byte[]) {
516                            // Deserialize ...
517                            ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value);
518                            ObjectInputStream ois = new ObjectInputStream(bais);
519                            value = ois.readObject();
520                            values.put(key, value);
521                        }
522                    }
523                }
524                String sourceName = (String)values.get(SOURCE_NAME);
525                String rootNodeUuidString = (String)values.get(ROOT_NODE_UUID);
526                String uuidPropertyName = (String)values.get(UUID_PROPERTY_NAME);
527                String cacheJndiName = (String)values.get(CACHE_JNDI_NAME);
528                String cacheFactoryJndiName = (String)values.get(CACHE_FACTORY_JNDI_NAME);
529                String cacheConfigurationName = (String)values.get(CACHE_CONFIGURATION_NAME);
530                Object defaultCachePolicy = values.get(DEFAULT_CACHE_POLICY);
531                String retryLimit = (String)values.get(RETRY_LIMIT);
532    
533                // Create the source instance ...
534                JBossCacheSource source = new JBossCacheSource();
535                if (sourceName != null) source.setName(sourceName);
536                if (rootNodeUuidString != null) source.setRootNodeUuid(rootNodeUuidString);
537                if (uuidPropertyName != null) source.setUuidPropertyName(uuidPropertyName);
538                if (cacheJndiName != null) source.setCacheJndiName(cacheJndiName);
539                if (cacheFactoryJndiName != null) source.setCacheFactoryJndiName(cacheFactoryJndiName);
540                if (cacheConfigurationName != null) source.setCacheConfigurationName(cacheConfigurationName);
541                if (defaultCachePolicy instanceof CachePolicy) {
542                    source.setDefaultCachePolicy((CachePolicy)defaultCachePolicy);
543                }
544                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
545                return source;
546            }
547            return null;
548        }
549    
550        /**
551         * {@inheritDoc}
552         * 
553         * @see org.jboss.dna.graph.connectors.RepositorySource#getCapabilities()
554         */
555        public RepositorySourceCapabilities getCapabilities() {
556            return CAPABILITIES;
557        }
558    }