Merge "fix bug 431"
[controller.git] / opendaylight / config / config-manager / src / main / java / org / opendaylight / controller / config / manager / impl / ConfigTransactionControllerImpl.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.config.manager.impl;
9
10 import org.opendaylight.controller.config.api.DependencyResolver;
11 import org.opendaylight.controller.config.api.ModuleIdentifier;
12 import org.opendaylight.controller.config.api.ServiceReferenceWritableRegistry;
13 import org.opendaylight.controller.config.api.ValidationException;
14 import org.opendaylight.controller.config.api.jmx.ObjectNameUtil;
15 import org.opendaylight.controller.config.manager.impl.dependencyresolver.DependencyResolverManager;
16 import org.opendaylight.controller.config.manager.impl.dynamicmbean.DynamicWritableWrapper;
17 import org.opendaylight.controller.config.manager.impl.dynamicmbean.ReadOnlyAtomicBoolean;
18 import org.opendaylight.controller.config.manager.impl.dynamicmbean.ReadOnlyAtomicBoolean.ReadOnlyAtomicBooleanImpl;
19 import org.opendaylight.controller.config.manager.impl.factoriesresolver.HierarchicalConfigMBeanFactoriesHolder;
20 import org.opendaylight.controller.config.manager.impl.jmx.TransactionModuleJMXRegistrator;
21 import org.opendaylight.controller.config.manager.impl.jmx.TransactionModuleJMXRegistrator.TransactionModuleJMXRegistration;
22 import org.opendaylight.controller.config.spi.Module;
23 import org.opendaylight.controller.config.spi.ModuleFactory;
24 import org.opendaylight.yangtools.concepts.Identifiable;
25 import org.opendaylight.yangtools.yang.data.impl.codec.CodecRegistry;
26 import org.osgi.framework.BundleContext;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 import javax.annotation.Nullable;
31 import javax.annotation.concurrent.GuardedBy;
32 import javax.management.DynamicMBean;
33 import javax.management.InstanceAlreadyExistsException;
34 import javax.management.InstanceNotFoundException;
35 import javax.management.MBeanServer;
36 import javax.management.ObjectName;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Set;
44 import java.util.concurrent.atomic.AtomicBoolean;
45
46 import static java.lang.String.format;
47
48 /**
49  * This is a JMX bean representing current transaction. It contains
50  * transaction identifier, unique version and parent version for
51  * optimistic locking.
52  */
53 class ConfigTransactionControllerImpl implements
54         ConfigTransactionControllerInternal,
55         ConfigTransactionControllerImplMXBean,
56         Identifiable<TransactionIdentifier> {
57     private static final Logger logger = LoggerFactory.getLogger(ConfigTransactionControllerImpl.class);
58
59     private final ConfigTransactionLookupRegistry txLookupRegistry;
60     private final ObjectName controllerON;
61
62     private final long parentVersion, currentVersion;
63     private final HierarchicalConfigMBeanFactoriesHolder factoriesHolder;
64     private final DependencyResolverManager dependencyResolverManager;
65     private final TransactionStatus transactionStatus;
66     private final MBeanServer transactionsMBeanServer;
67     private final Map<String, Map.Entry<ModuleFactory, BundleContext>> currentlyRegisteredFactories;
68
69     /**
70      * Disables ability of {@link DynamicWritableWrapper} to change attributes
71      * during validation.
72      */
73     @GuardedBy("this")
74     private final AtomicBoolean configBeanModificationDisabled = new AtomicBoolean(
75             false);
76     private final ReadOnlyAtomicBoolean readOnlyAtomicBoolean = new ReadOnlyAtomicBooleanImpl(
77             configBeanModificationDisabled);
78     private final MBeanServer configMBeanServer;
79
80     private final boolean blankTransaction;
81
82     @GuardedBy("this")
83     private final ServiceReferenceWritableRegistry writableSRRegistry;
84
85     public ConfigTransactionControllerImpl(ConfigTransactionLookupRegistry txLookupRegistry,
86                                            long parentVersion, CodecRegistry codecRegistry, long currentVersion,
87                                            Map<String, Entry<ModuleFactory, BundleContext>> currentlyRegisteredFactories,
88                                            MBeanServer transactionsMBeanServer, MBeanServer configMBeanServer,
89                                            boolean blankTransaction, ServiceReferenceWritableRegistry writableSRRegistry) {
90         this.txLookupRegistry = txLookupRegistry;
91         String transactionName = txLookupRegistry.getTransactionIdentifier().getName();
92         this.controllerON = ObjectNameUtil.createTransactionControllerON(transactionName);
93         this.parentVersion = parentVersion;
94         this.currentVersion = currentVersion;
95         this.currentlyRegisteredFactories = currentlyRegisteredFactories;
96         this.factoriesHolder = new HierarchicalConfigMBeanFactoriesHolder(currentlyRegisteredFactories);
97         this.transactionStatus = new TransactionStatus();
98         this.dependencyResolverManager = new DependencyResolverManager(transactionName, transactionStatus, writableSRRegistry, codecRegistry);
99         this.transactionsMBeanServer = transactionsMBeanServer;
100         this.configMBeanServer = configMBeanServer;
101         this.blankTransaction = blankTransaction;
102         this.writableSRRegistry = writableSRRegistry;
103     }
104
105     @Override
106     public void copyExistingModulesAndProcessFactoryDiff(Collection<ModuleInternalInfo> existingModules, List<ModuleFactory> lastListOfFactories) {
107         // copy old configuration to this server
108         for (ModuleInternalInfo oldConfigInfo : existingModules) {
109             try {
110                 copyExistingModule(oldConfigInfo);
111             } catch (InstanceAlreadyExistsException e) {
112                 throw new IllegalStateException("Error while copying " + oldConfigInfo, e);
113             }
114         }
115         processDefaultBeans(lastListOfFactories);
116     }
117
118     private synchronized void processDefaultBeans(List<ModuleFactory> lastListOfFactories) {
119         transactionStatus.checkNotCommitStarted();
120         transactionStatus.checkNotAborted();
121
122         Set<ModuleFactory> oldSet = new HashSet<>(lastListOfFactories);
123         Set<ModuleFactory> newSet = new HashSet<>(factoriesHolder.getModuleFactories());
124
125         List<ModuleFactory> toBeAdded = new ArrayList<>();
126         List<ModuleFactory> toBeRemoved = new ArrayList<>();
127         for (ModuleFactory moduleFactory : factoriesHolder.getModuleFactories()) {
128             if (oldSet.contains(moduleFactory) == false) {
129                 toBeAdded.add(moduleFactory);
130             }
131         }
132         for (ModuleFactory moduleFactory : lastListOfFactories) {
133             if (newSet.contains(moduleFactory) == false) {
134                 toBeRemoved.add(moduleFactory);
135             }
136         }
137         // add default modules
138         for (ModuleFactory moduleFactory : toBeAdded) {
139             Set<? extends Module> defaultModules = moduleFactory.getDefaultModules(dependencyResolverManager,
140                     getModuleFactoryBundleContext(moduleFactory.getImplementationName()));
141             for (Module module : defaultModules) {
142                 // ensure default module to be registered to jmx even if its module factory does not use dependencyResolverFactory
143                 DependencyResolver dependencyResolver = dependencyResolverManager.getOrCreate(module.getIdentifier());
144                 try {
145                     boolean defaultBean = true;
146                     putConfigBeanToJMXAndInternalMaps(module.getIdentifier(), module, moduleFactory, null, dependencyResolver, defaultBean);
147                 } catch (InstanceAlreadyExistsException e) {
148                     throw new IllegalStateException(e);
149                 }
150             }
151         }
152
153         // remove modules belonging to removed factories
154         for (ModuleFactory removedFactory : toBeRemoved) {
155             List<ModuleIdentifier> modulesOfRemovedFactory = dependencyResolverManager.findAllByFactory(removedFactory);
156             for (ModuleIdentifier name : modulesOfRemovedFactory) {
157                 destroyModule(name);
158             }
159         }
160     }
161
162
163     private synchronized void copyExistingModule(
164             ModuleInternalInfo oldConfigBeanInfo)
165             throws InstanceAlreadyExistsException {
166         transactionStatus.checkNotCommitStarted();
167         transactionStatus.checkNotAborted();
168         ModuleIdentifier moduleIdentifier = oldConfigBeanInfo.getIdentifier();
169         dependencyResolverManager.assertNotExists(moduleIdentifier);
170
171         ModuleFactory moduleFactory = factoriesHolder
172                 .findByModuleName(moduleIdentifier.getFactoryName());
173
174         Module module;
175         DependencyResolver dependencyResolver = dependencyResolverManager.getOrCreate(moduleIdentifier);
176         try {
177             BundleContext bc = getModuleFactoryBundleContext(moduleFactory.getImplementationName());
178             module = moduleFactory.createModule(
179                     moduleIdentifier.getInstanceName(), dependencyResolver,
180                     oldConfigBeanInfo.getReadableModule(), bc);
181         } catch (Exception e) {
182             throw new IllegalStateException(format(
183                     "Error while copying old configuration from %s to %s",
184                     oldConfigBeanInfo, moduleFactory), e);
185         }
186         putConfigBeanToJMXAndInternalMaps(moduleIdentifier, module, moduleFactory, oldConfigBeanInfo, dependencyResolver,
187                 oldConfigBeanInfo.isDefaultBean());
188     }
189
190     @Override
191     public synchronized ObjectName createModule(String factoryName,
192                                                 String instanceName) throws InstanceAlreadyExistsException {
193
194         transactionStatus.checkNotCommitStarted();
195         transactionStatus.checkNotAborted();
196         ModuleIdentifier moduleIdentifier = new ModuleIdentifier(factoryName, instanceName);
197         dependencyResolverManager.assertNotExists(moduleIdentifier);
198
199         // find factory
200         ModuleFactory moduleFactory = factoriesHolder.findByModuleName(factoryName);
201         DependencyResolver dependencyResolver = dependencyResolverManager.getOrCreate(moduleIdentifier);
202         Module module = moduleFactory.createModule(instanceName, dependencyResolver,
203                 getModuleFactoryBundleContext(moduleFactory.getImplementationName()));
204         boolean defaultBean = false;
205         return putConfigBeanToJMXAndInternalMaps(moduleIdentifier, module,
206                 moduleFactory, null, dependencyResolver, defaultBean);
207     }
208
209     private synchronized ObjectName putConfigBeanToJMXAndInternalMaps(
210             ModuleIdentifier moduleIdentifier, Module module,
211             ModuleFactory moduleFactory,
212             @Nullable ModuleInternalInfo maybeOldConfigBeanInfo, DependencyResolver dependencyResolver, boolean isDefaultBean)
213             throws InstanceAlreadyExistsException {
214
215         logger.debug("Adding module {} to transaction {}", moduleIdentifier, this);
216         if (moduleIdentifier.equals(module.getIdentifier()) == false) {
217             throw new IllegalStateException("Incorrect name reported by module. Expected "
218                     + moduleIdentifier + ", got " + module.getIdentifier());
219         }
220         if (dependencyResolver.getIdentifier().equals(moduleIdentifier) == false) {
221             throw new IllegalStateException("Incorrect name reported by dependency resolver. Expected "
222                     + moduleIdentifier + ", got " + dependencyResolver.getIdentifier());
223         }
224         DynamicMBean writableDynamicWrapper = new DynamicWritableWrapper(
225                 module, moduleIdentifier, getTransactionIdentifier(),
226                 readOnlyAtomicBoolean, transactionsMBeanServer,
227                 configMBeanServer);
228
229         ObjectName writableON = ObjectNameUtil.createTransactionModuleON(
230                 getTransactionIdentifier().getName(), moduleIdentifier);
231         // put wrapper to jmx
232         TransactionModuleJMXRegistration transactionModuleJMXRegistration = getTxModuleJMXRegistrator()
233                 .registerMBean(writableDynamicWrapper, writableON);
234         ModuleInternalTransactionalInfo moduleInternalTransactionalInfo = new ModuleInternalTransactionalInfo(
235                 moduleIdentifier, module, moduleFactory,
236                 maybeOldConfigBeanInfo, transactionModuleJMXRegistration, isDefaultBean);
237
238         dependencyResolverManager.put(moduleInternalTransactionalInfo);
239         return writableON;
240     }
241
242     @Override
243     public synchronized void destroyModule(ObjectName objectName) throws InstanceNotFoundException {
244         checkTransactionName(objectName);
245         ObjectNameUtil.checkDomain(objectName);
246         ModuleIdentifier moduleIdentifier = ObjectNameUtil.fromON(objectName,
247                 ObjectNameUtil.TYPE_MODULE);
248         destroyModule(moduleIdentifier);
249     }
250
251     private void checkTransactionName(ObjectName objectName) {
252         String foundTransactionName = ObjectNameUtil
253                 .getTransactionName(objectName);
254         if (getTransactionIdentifier().getName().equals(foundTransactionName) == false) {
255             throw new IllegalArgumentException("Wrong transaction name "
256                     + objectName);
257         }
258     }
259
260     private synchronized void destroyModule(ModuleIdentifier moduleIdentifier) {
261         logger.debug("Destroying module {} in transaction {}", moduleIdentifier, this);
262         transactionStatus.checkNotAborted();
263
264         if (blankTransaction == false) {
265             ModuleInternalTransactionalInfo found =
266                     dependencyResolverManager.findModuleInternalTransactionalInfo(moduleIdentifier);
267             if (found.isDefaultBean()) {
268                 logger.warn("Warning: removing default bean. This will be forbidden in next version of config-subsystem");
269             }
270         }
271         // first remove refNames, it checks for objectname existence
272         try {
273             writableSRRegistry.removeServiceReferences(
274                     ObjectNameUtil.createTransactionModuleON(getTransactionName(), moduleIdentifier));
275         } catch (InstanceNotFoundException e) {
276             logger.error("Possible code error: cannot find {} in {}", moduleIdentifier, writableSRRegistry);
277             throw new IllegalStateException("Possible code error: cannot find " + moduleIdentifier, e);
278         }
279
280         ModuleInternalTransactionalInfo removedTInfo = dependencyResolverManager.destroyModule(moduleIdentifier);
281         // remove from jmx
282         removedTInfo.getTransactionModuleJMXRegistration().close();
283     }
284
285     @Override
286     public long getParentVersion() {
287         return parentVersion;
288     }
289
290     @Override
291     public long getVersion() {
292         return currentVersion;
293     }
294
295     @Override
296     public synchronized void validateConfig() throws ValidationException {
297         if (configBeanModificationDisabled.get()) {
298             throw new IllegalStateException("Cannot start validation");
299         }
300         configBeanModificationDisabled.set(true);
301         try {
302             validate_noLocks();
303         } finally {
304             configBeanModificationDisabled.set(false);
305         }
306     }
307
308     private void validate_noLocks() throws ValidationException {
309         transactionStatus.checkNotAborted();
310         logger.trace("Validating transaction {}", getTransactionIdentifier());
311         // call validate()
312         List<ValidationException> collectedExceptions = new ArrayList<>();
313         for (Entry<ModuleIdentifier, Module> entry : dependencyResolverManager
314                 .getAllModules().entrySet()) {
315             ModuleIdentifier name = entry.getKey();
316             Module module = entry.getValue();
317             try {
318                 module.validate();
319             } catch (Exception e) {
320                 logger.warn("Validation exception in {}", getTransactionName(),
321                         e);
322                 collectedExceptions.add(ValidationException
323                         .createForSingleException(name, e));
324             }
325         }
326         if (collectedExceptions.size() > 0) {
327             throw ValidationException
328                     .createFromCollectedValidationExceptions(collectedExceptions);
329         }
330         logger.trace("Validated transaction {}", getTransactionIdentifier());
331     }
332
333     /**
334      * If this method passes validation, it will grab
335      * {@link TransactionStatus#secondPhaseCommitStarted} lock. This lock will
336      * prevent calling @{link #validateBeforeCommitAndLockTransaction},
337      * effectively only allowing to call {@link #secondPhaseCommit} after
338      * successful return of this method.
339      */
340     @Override
341     public synchronized CommitInfo validateBeforeCommitAndLockTransaction()
342             throws ValidationException {
343         transactionStatus.checkNotAborted();
344         transactionStatus.checkNotCommitStarted();
345         configBeanModificationDisabled.set(true);
346         try {
347             validate_noLocks();
348         } catch (ValidationException e) {
349             logger.trace("Commit failed on validation");
350             configBeanModificationDisabled.set(false); // recoverable error
351             throw e;
352         }
353         // errors in this state are not recoverable. modules are not mutable
354         // anymore.
355         transactionStatus.setSecondPhaseCommitStarted();
356         return dependencyResolverManager.toCommitInfo();
357     }
358
359     /**
360      * {@inheritDoc}
361      */
362     @Override
363     public synchronized List<ModuleIdentifier> secondPhaseCommit() {
364         transactionStatus.checkNotAborted();
365         transactionStatus.checkCommitStarted();
366         if (configBeanModificationDisabled.get() == false) {
367             throw new IllegalStateException(
368                     "Internal error - validateBeforeCommitAndLockTransaction should be called "
369                             + "to obtain a lock");
370         }
371
372         logger.trace("Committing transaction {}", getTransactionIdentifier());
373
374         // call getInstance()
375         for (Entry<ModuleIdentifier, Module> entry : dependencyResolverManager
376                 .getAllModules().entrySet()) {
377             Module module = entry.getValue();
378             ModuleIdentifier name = entry.getKey();
379             try {
380                 logger.debug("About to commit {} in transaction {}",
381                         name, getTransactionIdentifier());
382                 module.getInstance();
383             } catch (Exception e) {
384                 logger.error("Commit failed on {} in transaction {}", name,
385                         getTransactionIdentifier(), e);
386                 internalAbort();
387                 throw new IllegalStateException(
388                         format("Error - getInstance() failed for %s in transaction %s",
389                                 name, getTransactionIdentifier()), e);
390             }
391         }
392
393         // count dependency order
394
395         logger.trace("Committed configuration {}", getTransactionIdentifier());
396         transactionStatus.setCommitted();
397         // unregister this and all modules from jmx
398         close();
399
400         return dependencyResolverManager.getSortedModuleIdentifiers();
401     }
402
403     @Override
404     public synchronized void abortConfig() {
405         transactionStatus.checkNotCommitStarted();
406         transactionStatus.checkNotAborted();
407         internalAbort();
408     }
409
410     private void internalAbort() {
411         transactionStatus.setAborted();
412         close();
413     }
414
415     public void close() {
416         //FIXME: should not close object that was retrieved in constructor, a wrapper object should do that perhaps
417         txLookupRegistry.close();
418     }
419
420     @Override
421     public ObjectName getControllerObjectName() {
422         return controllerON;
423     }
424
425     @Override
426     public String getTransactionName() {
427         return getTransactionIdentifier().getName();
428     }
429
430     /**
431      * {@inheritDoc}
432      */
433     @Override
434     public Set<ObjectName> lookupConfigBeans() {
435         return txLookupRegistry.lookupConfigBeans();
436     }
437
438     /**
439      * {@inheritDoc}
440      */
441     @Override
442     public Set<ObjectName> lookupConfigBeans(String moduleName) {
443         return txLookupRegistry.lookupConfigBeans(moduleName);
444     }
445
446     /**
447      * {@inheritDoc}
448      */
449     @Override
450     public ObjectName lookupConfigBean(String moduleName, String instanceName)
451             throws InstanceNotFoundException {
452         return txLookupRegistry.lookupConfigBean(moduleName, instanceName);
453     }
454
455     /**
456      * {@inheritDoc}
457      */
458     @Override
459     public Set<ObjectName> lookupConfigBeans(String moduleName, String instanceName) {
460         return txLookupRegistry.lookupConfigBeans(moduleName, instanceName);
461     }
462
463     /**
464      * {@inheritDoc}
465      */
466     @Override
467     public void checkConfigBeanExists(ObjectName objectName) throws InstanceNotFoundException {
468         txLookupRegistry.checkConfigBeanExists(objectName);
469     }
470     // --
471
472     /**
473      * {@inheritDoc}
474      */
475     @Override
476     public Set<String> getAvailableModuleNames() {
477         return factoriesHolder.getModuleNames();
478     }
479
480     @Override
481     public boolean isClosed() {
482         return transactionStatus.isAbortedOrCommitted();
483     }
484
485     @Override
486     public String toString() {
487         StringBuilder sb = new StringBuilder();
488         sb.append("transactionName=");
489         sb.append(getTransactionName());
490         return sb.toString();
491     }
492
493     // @VisibleForTesting
494
495     TransactionModuleJMXRegistrator getTxModuleJMXRegistrator() {
496         return txLookupRegistry.getTxModuleJMXRegistrator();
497     }
498
499     public TransactionIdentifier getName() {
500         return getTransactionIdentifier();
501     }
502
503     @Override
504     public List<ModuleFactory> getCurrentlyRegisteredFactories() {
505         return new ArrayList<>(factoriesHolder.getModuleFactories());
506     }
507
508     @Override
509     public TransactionIdentifier getIdentifier() {
510         return getTransactionIdentifier();
511     }
512
513     @Override
514     public BundleContext getModuleFactoryBundleContext(String factoryName) {
515         Map.Entry<ModuleFactory, BundleContext> factoryBundleContextEntry = this.currentlyRegisteredFactories.get(factoryName);
516         if (factoryBundleContextEntry == null || factoryBundleContextEntry.getValue() == null) {
517             throw new NullPointerException("Bundle context of " + factoryName + " ModuleFactory not found.");
518         }
519         return factoryBundleContextEntry.getValue();
520     }
521
522     // service reference functionality:
523
524
525     @Override
526     public synchronized ObjectName lookupConfigBeanByServiceInterfaceName(String serviceInterfaceQName, String refName) {
527         return writableSRRegistry.lookupConfigBeanByServiceInterfaceName(serviceInterfaceQName, refName);
528     }
529
530     @Override
531     public synchronized Map<String, Map<String, ObjectName>> getServiceMapping() {
532         return writableSRRegistry.getServiceMapping();
533     }
534
535     @Override
536     public synchronized Map<String, ObjectName> lookupServiceReferencesByServiceInterfaceName(String serviceInterfaceQName) {
537         return writableSRRegistry.lookupServiceReferencesByServiceInterfaceName(serviceInterfaceQName);
538     }
539
540     @Override
541     public synchronized Set<String> lookupServiceInterfaceNames(ObjectName objectName) throws InstanceNotFoundException {
542         return writableSRRegistry.lookupServiceInterfaceNames(objectName);
543     }
544
545     @Override
546     public synchronized String getServiceInterfaceName(String namespace, String localName) {
547         return writableSRRegistry.getServiceInterfaceName(namespace, localName);
548     }
549
550     @Override
551     public synchronized ObjectName saveServiceReference(String serviceInterfaceName, String refName, ObjectName moduleON) throws InstanceNotFoundException {
552         return writableSRRegistry.saveServiceReference(serviceInterfaceName, refName, moduleON);
553     }
554
555     @Override
556     public synchronized void removeServiceReference(String serviceInterfaceName, String refName) throws InstanceNotFoundException {
557         writableSRRegistry.removeServiceReference(serviceInterfaceName, refName);
558     }
559
560     @Override
561     public synchronized void removeAllServiceReferences() {
562         writableSRRegistry.removeAllServiceReferences();
563     }
564
565     @Override
566     public boolean removeServiceReferences(ObjectName objectName) throws InstanceNotFoundException {
567         return writableSRRegistry.removeServiceReferences(objectName);
568     }
569
570     @Override
571     public ServiceReferenceWritableRegistry getWritableRegistry() {
572         return writableSRRegistry;
573     }
574
575     public TransactionIdentifier getTransactionIdentifier() {
576         return txLookupRegistry.getTransactionIdentifier();
577     }
578
579     @Override
580     public Set<String> getAvailableModuleFactoryQNames() {
581         return txLookupRegistry.getAvailableModuleFactoryQNames();
582     }
583
584     @Override
585     public void checkServiceReferenceExists(ObjectName objectName) throws InstanceNotFoundException {
586         writableSRRegistry.checkServiceReferenceExists(objectName);
587     }
588
589     @Override
590     public ObjectName getServiceReference(String serviceInterfaceQName, String refName) throws InstanceNotFoundException {
591         return writableSRRegistry.getServiceReference(serviceInterfaceQName, refName);
592     }
593 }