Add support for identity-ref config attributes to config/netconf subsystem
[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         configBeanModificationDisabled.set(true);
300         try {
301             validate_noLocks();
302         } finally {
303             configBeanModificationDisabled.set(false);
304         }
305     }
306
307     private void validate_noLocks() throws ValidationException {
308         transactionStatus.checkNotAborted();
309         logger.trace("Validating transaction {}", getTransactionIdentifier());
310         // call validate()
311         List<ValidationException> collectedExceptions = new ArrayList<>();
312         for (Entry<ModuleIdentifier, Module> entry : dependencyResolverManager
313                 .getAllModules().entrySet()) {
314             ModuleIdentifier name = entry.getKey();
315             Module module = entry.getValue();
316             try {
317                 module.validate();
318             } catch (Exception e) {
319                 logger.warn("Validation exception in {}", getTransactionName(),
320                         e);
321                 collectedExceptions.add(ValidationException
322                         .createForSingleException(name, e));
323             }
324         }
325         if (collectedExceptions.size() > 0) {
326             throw ValidationException
327                     .createFromCollectedValidationExceptions(collectedExceptions);
328         }
329         logger.trace("Validated transaction {}", getTransactionIdentifier());
330     }
331
332     /**
333      * If this method passes validation, it will grab
334      * {@link TransactionStatus#secondPhaseCommitStarted} lock. This lock will
335      * prevent calling @{link #validateBeforeCommitAndLockTransaction},
336      * effectively only allowing to call {@link #secondPhaseCommit} after
337      * successful return of this method.
338      */
339     @Override
340     public synchronized CommitInfo validateBeforeCommitAndLockTransaction()
341             throws ValidationException {
342         transactionStatus.checkNotAborted();
343         transactionStatus.checkNotCommitStarted();
344         configBeanModificationDisabled.set(true);
345         try {
346             validate_noLocks();
347         } catch (ValidationException e) {
348             logger.trace("Commit failed on validation");
349             configBeanModificationDisabled.set(false); // recoverable error
350             throw e;
351         }
352         // errors in this state are not recoverable. modules are not mutable
353         // anymore.
354         transactionStatus.setSecondPhaseCommitStarted();
355         return dependencyResolverManager.toCommitInfo();
356     }
357
358     /**
359      * {@inheritDoc}
360      */
361     @Override
362     public synchronized List<ModuleIdentifier> secondPhaseCommit() {
363         transactionStatus.checkNotAborted();
364         transactionStatus.checkCommitStarted();
365         if (configBeanModificationDisabled.get() == false) {
366             throw new IllegalStateException(
367                     "Internal error - validateBeforeCommitAndLockTransaction should be called "
368                             + "to obtain a lock");
369         }
370
371         logger.trace("Committing transaction {}", getTransactionIdentifier());
372
373         // call getInstance()
374         for (Entry<ModuleIdentifier, Module> entry : dependencyResolverManager
375                 .getAllModules().entrySet()) {
376             Module module = entry.getValue();
377             ModuleIdentifier name = entry.getKey();
378             try {
379                 logger.debug("About to commit {} in transaction {}",
380                         name, getTransactionIdentifier());
381                 module.getInstance();
382             } catch (Exception e) {
383                 logger.error("Commit failed on {} in transaction {}", name,
384                         getTransactionIdentifier(), e);
385                 internalAbort();
386                 throw new RuntimeException(
387                         format("Error - getInstance() failed for %s in transaction %s",
388                                 name, getTransactionIdentifier()), e);
389             }
390         }
391
392         // count dependency order
393
394         logger.trace("Committed configuration {}", getTransactionIdentifier());
395         transactionStatus.setCommitted();
396         // unregister this and all modules from jmx
397         close();
398
399         return dependencyResolverManager.getSortedModuleIdentifiers();
400     }
401
402     @Override
403     public synchronized void abortConfig() {
404         transactionStatus.checkNotCommitStarted();
405         transactionStatus.checkNotAborted();
406         internalAbort();
407     }
408
409     private void internalAbort() {
410         transactionStatus.setAborted();
411         close();
412     }
413
414     public void close() {
415         //FIXME: should not close object that was retrieved in constructor, a wrapper object should do that perhaps
416         txLookupRegistry.close();
417     }
418
419     @Override
420     public ObjectName getControllerObjectName() {
421         return controllerON;
422     }
423
424     @Override
425     public String getTransactionName() {
426         return getTransactionIdentifier().getName();
427     }
428
429     /**
430      * {@inheritDoc}
431      */
432     @Override
433     public Set<ObjectName> lookupConfigBeans() {
434         return txLookupRegistry.lookupConfigBeans();
435     }
436
437     /**
438      * {@inheritDoc}
439      */
440     @Override
441     public Set<ObjectName> lookupConfigBeans(String moduleName) {
442         return txLookupRegistry.lookupConfigBeans(moduleName);
443     }
444
445     /**
446      * {@inheritDoc}
447      */
448     @Override
449     public ObjectName lookupConfigBean(String moduleName, String instanceName)
450             throws InstanceNotFoundException {
451         return txLookupRegistry.lookupConfigBean(moduleName, instanceName);
452     }
453
454     /**
455      * {@inheritDoc}
456      */
457     @Override
458     public Set<ObjectName> lookupConfigBeans(String moduleName, String instanceName) {
459         return txLookupRegistry.lookupConfigBeans(moduleName, instanceName);
460     }
461
462     /**
463      * {@inheritDoc}
464      */
465     @Override
466     public void checkConfigBeanExists(ObjectName objectName) throws InstanceNotFoundException {
467         txLookupRegistry.checkConfigBeanExists(objectName);
468     }
469     // --
470
471     /**
472      * {@inheritDoc}
473      */
474     @Override
475     public Set<String> getAvailableModuleNames() {
476         return factoriesHolder.getModuleNames();
477     }
478
479     @Override
480     public boolean isClosed() {
481         return transactionStatus.isAbortedOrCommitted();
482     }
483
484     @Override
485     public String toString() {
486         StringBuilder sb = new StringBuilder();
487         sb.append("transactionName=");
488         sb.append(getTransactionName());
489         return sb.toString();
490     }
491
492     // @VisibleForTesting
493
494     TransactionModuleJMXRegistrator getTxModuleJMXRegistrator() {
495         return txLookupRegistry.getTxModuleJMXRegistrator();
496     }
497
498     public TransactionIdentifier getName() {
499         return getTransactionIdentifier();
500     }
501
502     @Override
503     public List<ModuleFactory> getCurrentlyRegisteredFactories() {
504         return new ArrayList<>(factoriesHolder.getModuleFactories());
505     }
506
507     @Override
508     public TransactionIdentifier getIdentifier() {
509         return getTransactionIdentifier();
510     }
511
512     @Override
513     public BundleContext getModuleFactoryBundleContext(String factoryName) {
514         Map.Entry<ModuleFactory, BundleContext> factoryBundleContextEntry = this.currentlyRegisteredFactories.get(factoryName);
515         if (factoryBundleContextEntry == null || factoryBundleContextEntry.getValue() == null) {
516             throw new NullPointerException("Bundle context of " + factoryName + " ModuleFactory not found.");
517         }
518         return factoryBundleContextEntry.getValue();
519     }
520
521     // service reference functionality:
522
523
524     @Override
525     public synchronized ObjectName lookupConfigBeanByServiceInterfaceName(String serviceInterfaceQName, String refName) {
526         return writableSRRegistry.lookupConfigBeanByServiceInterfaceName(serviceInterfaceQName, refName);
527     }
528
529     @Override
530     public synchronized Map<String, Map<String, ObjectName>> getServiceMapping() {
531         return writableSRRegistry.getServiceMapping();
532     }
533
534     @Override
535     public synchronized Map<String, ObjectName> lookupServiceReferencesByServiceInterfaceName(String serviceInterfaceQName) {
536         return writableSRRegistry.lookupServiceReferencesByServiceInterfaceName(serviceInterfaceQName);
537     }
538
539     @Override
540     public synchronized Set<String> lookupServiceInterfaceNames(ObjectName objectName) throws InstanceNotFoundException {
541         return writableSRRegistry.lookupServiceInterfaceNames(objectName);
542     }
543
544     @Override
545     public synchronized String getServiceInterfaceName(String namespace, String localName) {
546         return writableSRRegistry.getServiceInterfaceName(namespace, localName);
547     }
548
549     @Override
550     public synchronized ObjectName saveServiceReference(String serviceInterfaceName, String refName, ObjectName moduleON) throws InstanceNotFoundException {
551         return writableSRRegistry.saveServiceReference(serviceInterfaceName, refName, moduleON);
552     }
553
554     @Override
555     public synchronized void removeServiceReference(String serviceInterfaceName, String refName) throws InstanceNotFoundException {
556         writableSRRegistry.removeServiceReference(serviceInterfaceName, refName);
557     }
558
559     @Override
560     public synchronized void removeAllServiceReferences() {
561         writableSRRegistry.removeAllServiceReferences();
562     }
563
564     @Override
565     public boolean removeServiceReferences(ObjectName objectName) throws InstanceNotFoundException {
566         return writableSRRegistry.removeServiceReferences(objectName);
567     }
568
569     @Override
570     public ServiceReferenceWritableRegistry getWritableRegistry() {
571         return writableSRRegistry;
572     }
573
574     public TransactionIdentifier getTransactionIdentifier() {
575         return txLookupRegistry.getTransactionIdentifier();
576     }
577
578     @Override
579     public Set<String> getAvailableModuleFactoryQNames() {
580         return txLookupRegistry.getAvailableModuleFactoryQNames();
581     }
582
583     @Override
584     public void checkServiceReferenceExists(ObjectName objectName) throws InstanceNotFoundException {
585         writableSRRegistry.checkServiceReferenceExists(objectName);
586     }
587
588     @Override
589     public ObjectName getServiceReference(String serviceInterfaceQName, String refName) throws InstanceNotFoundException {
590         return writableSRRegistry.getServiceReference(serviceInterfaceQName, refName);
591     }
592 }