c771d80ad78f6eac011b354d6760df911c8622cf
[music.git] / src / main / java / org / onap / music / datastore / MusicDataStore.java
1 /*
2  * ============LICENSE_START==========================================
3  * org.onap.music
4  * ===================================================================
5  *  Copyright (c) 2017 AT&T Intellectual Property
6  * ===================================================================
7  *  Modifications Copyright (c) 2018-2019 IBM
8  *  Modifications Copyright (c) 2019 Samsung
9  * ===================================================================
10  *  Licensed under the Apache License, Version 2.0 (the "License");
11  *  you may not use this file except in compliance with the License.
12  *  You may obtain a copy of the License at
13  *
14  *     http://www.apache.org/licenses/LICENSE-2.0
15  *
16  *  Unless required by applicable law or agreed to in writing, software
17  *  distributed under the License is distributed on an "AS IS" BASIS,
18  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19  *  See the License for the specific language governing permissions and
20  *  limitations under the License.
21  *
22  * ============LICENSE_END=============================================
23  * ====================================================================
24  */
25
26 package org.onap.music.datastore;
27
28 import java.net.InetAddress;
29 import java.net.NetworkInterface;
30 import java.net.SocketException;
31 import java.nio.ByteBuffer;
32 import java.util.ArrayList;
33 import java.util.Enumeration;
34 import java.util.HashMap;
35 import java.util.Iterator;
36 import java.util.Map;
37
38 import org.apache.commons.jcs.access.CacheAccess;
39 import org.onap.music.eelf.logging.EELFLoggerDelegate;
40 import org.onap.music.eelf.logging.format.AppMessages;
41 import org.onap.music.eelf.logging.format.ErrorSeverity;
42 import org.onap.music.eelf.logging.format.ErrorTypes;
43 import org.onap.music.exceptions.MusicQueryException;
44 import org.onap.music.exceptions.MusicServiceException;
45 import org.onap.music.main.MusicUtil;
46 import com.codahale.metrics.JmxReporter;
47 import com.datastax.driver.core.Cluster;
48 import com.datastax.driver.core.ColumnDefinitions;
49 import com.datastax.driver.core.ColumnDefinitions.Definition;
50 import com.datastax.driver.core.ConsistencyLevel;
51 import com.datastax.driver.core.DataType;
52 import com.datastax.driver.core.HostDistance;
53 import com.datastax.driver.core.KeyspaceMetadata;
54 import com.datastax.driver.core.Metadata;
55 import com.datastax.driver.core.PoolingOptions;
56 import com.datastax.driver.core.PreparedStatement;
57 import com.datastax.driver.core.ResultSet;
58 import com.datastax.driver.core.Row;
59 import com.datastax.driver.core.Session;
60 import com.datastax.driver.core.SimpleStatement;
61 import com.datastax.driver.core.TableMetadata;
62 import com.datastax.driver.core.exceptions.AlreadyExistsException;
63 import com.datastax.driver.core.exceptions.InvalidQueryException;
64 import com.datastax.driver.core.exceptions.NoHostAvailableException;
65 import com.sun.jersey.core.util.Base64;
66
67 /**
68  * @author nelson24
69  *
70  */
71 public class MusicDataStore {
72
73     public static final String CONSISTENCY_LEVEL_ONE = "ONE";
74     public static final String CONSISTENCY_LEVEL_QUORUM = "QUORUM";
75     private Session session;
76     private Cluster cluster;
77
78
79     /**
80      * @param session
81      */
82     public void setSession(Session session) {
83         this.session = session;
84     }
85
86     /**
87      * @param session
88      */
89     public Session getSession() {
90         return session;
91     }
92
93     /**
94      * @param cluster
95      */
96     public void setCluster(Cluster cluster) {
97         this.cluster = cluster;
98     }
99
100
101     private EELFLoggerDelegate logger = EELFLoggerDelegate.getLogger(MusicDataStore.class);
102
103     /**
104      *
105      */
106     public MusicDataStore() {
107         connectToCassaCluster();
108     }
109
110
111     /**
112      * @param cluster
113      * @param session
114      */
115     public MusicDataStore(Cluster cluster, Session session) {
116         this.session = session;
117         this.cluster = cluster;
118     }
119
120     /**
121      *
122      * @param remoteIp
123      * @throws MusicServiceException
124      */
125     public MusicDataStore(String remoteIp) {
126         try {
127             connectToCassaCluster(remoteIp);
128         } catch (MusicServiceException e) {
129             logger.error(EELFLoggerDelegate.errorLogger, e.getMessage(), e);
130         }
131     }
132
133     /**
134      *
135      * @return
136      */
137     private ArrayList<String> getAllPossibleLocalIps() {
138         ArrayList<String> allPossibleIps = new ArrayList<>();
139         try {
140             Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
141             while (en.hasMoreElements()) {
142                 NetworkInterface ni = en.nextElement();
143                 Enumeration<InetAddress> ee = ni.getInetAddresses();
144                 while (ee.hasMoreElements()) {
145                     InetAddress ia = ee.nextElement();
146                     allPossibleIps.add(ia.getHostAddress());
147                 }
148             }
149         } catch (SocketException e) {
150             logger.error(EELFLoggerDelegate.errorLogger, e.getMessage(), AppMessages.CONNCECTIVITYERROR,
151                 ErrorSeverity.ERROR, ErrorTypes.CONNECTIONERROR, e);
152         }catch(Exception e) {
153             logger.error(EELFLoggerDelegate.errorLogger, e.getMessage(), ErrorSeverity.ERROR, ErrorTypes
154                 .GENERALSERVICEERROR, e);
155         }
156         return allPossibleIps;
157     }
158
159     /**
160      * This method iterates through all available IP addresses and connects to multiple cassandra
161      * clusters.
162      */
163     private void connectToCassaCluster() {
164         Iterator<String> it = getAllPossibleLocalIps().iterator();
165         String address = "localhost";
166         String[] addresses = null;
167         address = MusicUtil.getMyCassaHost();
168         addresses = address.split(",");
169
170         logger.info(EELFLoggerDelegate.applicationLogger,
171                         "Connecting to cassa cluster: Iterating through possible ips:"
172                                         + getAllPossibleLocalIps());
173         PoolingOptions poolingOptions = new PoolingOptions();
174         poolingOptions
175         .setConnectionsPerHost(HostDistance.LOCAL,  4, 10)
176         .setConnectionsPerHost(HostDistance.REMOTE, 2, 4);
177         while (it.hasNext()) {
178             try {
179                 if(MusicUtil.getCassName() != null && MusicUtil.getCassPwd() != null) {
180                     logger.info(EELFLoggerDelegate.applicationLogger,
181                             "Building with credentials "+MusicUtil.getCassName()+" & "+MusicUtil.getCassPwd());
182                     cluster = Cluster.builder().withPort(MusicUtil.getCassandraPort())
183                                         .withCredentials(MusicUtil.getCassName(), MusicUtil.getCassPwd())
184                                         //.withLoadBalancingPolicy(new RoundRobinPolicy())
185                                         .withoutJMXReporting()
186                                         .withPoolingOptions(poolingOptions)
187                                         .addContactPoints(addresses).build();
188                 }
189                 else
190                     cluster = Cluster.builder().withPort(MusicUtil.getCassandraPort())
191                                         //.withLoadBalancingPolicy(new RoundRobinPolicy())
192                                         .addContactPoints(addresses).build();
193
194                 Metadata metadata = cluster.getMetadata();
195                 logger.info(EELFLoggerDelegate.applicationLogger, "Connected to cassa cluster "
196                                 + metadata.getClusterName() + " at " + address);
197                 session = cluster.connect();
198
199                 break;
200             } catch (NoHostAvailableException e) {
201                 address = it.next();
202                 logger.error(EELFLoggerDelegate.errorLogger, e.getMessage(),AppMessages.HOSTUNAVAILABLE,
203                     ErrorSeverity.ERROR, ErrorTypes.CONNECTIONERROR, e);
204             }
205         }
206     }
207
208     /**
209      *
210      */
211     public void close() {
212         session.close();
213     }
214
215     /**
216      * This method connects to cassandra cluster on specific address.
217      *
218      * @param address
219      */
220     private void connectToCassaCluster(String address) throws MusicServiceException {
221         String[] addresses = null;
222         addresses = address.split(",");
223         PoolingOptions poolingOptions = new PoolingOptions();
224         poolingOptions
225         .setConnectionsPerHost(HostDistance.LOCAL,  4, 10)
226         .setConnectionsPerHost(HostDistance.REMOTE, 2, 4);
227         if(MusicUtil.getCassName() != null && MusicUtil.getCassPwd() != null) {
228             logger.info(EELFLoggerDelegate.applicationLogger,
229                     "Building with credentials "+MusicUtil.getCassName()+" & "+MusicUtil.getCassPwd());
230             cluster = Cluster.builder().withPort(MusicUtil.getCassandraPort())
231                         .withCredentials(MusicUtil.getCassName(), MusicUtil.getCassPwd())
232                         //.withLoadBalancingPolicy(new RoundRobinPolicy())
233                         .withoutJMXReporting()
234                         .withPoolingOptions(poolingOptions)
235                         .addContactPoints(addresses).build();
236         } else {
237             cluster = Cluster.builder().withPort(MusicUtil.getCassandraPort())
238                         //.withLoadBalancingPolicy(new RoundRobinPolicy())
239                         .withoutJMXReporting()
240                         .withPoolingOptions(poolingOptions)
241                         .addContactPoints(addresses).build();
242         }
243         
244         // JmxReporter reporter =
245         //         JmxReporter.forRegistry(cluster.getMetrics().getRegistry())
246         //             .inDomain(cluster.getClusterName() + "-metrics")
247         //             .build();
248
249         //     reporter.start();
250             
251         Metadata metadata = cluster.getMetadata();
252         logger.info(EELFLoggerDelegate.applicationLogger, "Connected to cassa cluster "
253                         + metadata.getClusterName() + " at " + address);
254         try {
255             session = cluster.connect();
256         } catch (Exception ex) {
257             logger.error(EELFLoggerDelegate.errorLogger, ex.getMessage(),AppMessages.CASSANDRACONNECTIVITY,
258                 ErrorSeverity.ERROR, ErrorTypes.SERVICEUNAVAILABLE, ex);
259             throw new MusicServiceException(
260                             "Error while connecting to Cassandra cluster.. " + ex.getMessage());
261         }
262     }
263
264     /**
265      *
266      * @param keyspace
267      * @param tableName
268      * @param columnName
269      * @return DataType
270      */
271     public DataType returnColumnDataType(String keyspace, String tableName, String columnName) {
272         KeyspaceMetadata ks = cluster.getMetadata().getKeyspace(keyspace);
273         TableMetadata table = ks.getTable(tableName);
274         return table.getColumn(columnName).getType();
275
276     }
277
278     /**
279      *
280      * @param keyspace
281      * @param tableName
282      * @return TableMetadata
283      */
284     public TableMetadata returnColumnMetadata(String keyspace, String tableName) {
285         KeyspaceMetadata ks = cluster.getMetadata().getKeyspace(keyspace);
286         return ks.getTable(tableName);
287     }
288     
289     /**
290     *
291     * @param keyspace
292     * @param tableName
293     * @return TableMetadata
294     */
295    public KeyspaceMetadata returnKeyspaceMetadata(String keyspace) {
296        KeyspaceMetadata ks = cluster.getMetadata().getKeyspace(keyspace);
297        return ks;
298    }
299
300
301     /**
302      * Utility function to return the Java specific object type.
303      *
304      * @param row
305      * @param colName
306      * @param colType
307      * @return
308      */
309     public Object getColValue(Row row, String colName, DataType colType) {
310
311         switch (colType.getName()) {
312             case VARCHAR:
313                 return row.getString(colName);
314             case UUID:
315                 return row.getUUID(colName);
316             case VARINT:
317                 return row.getVarint(colName);
318             case BIGINT:
319                 return row.getLong(colName);
320             case INT:
321                 return row.getInt(colName);
322             case FLOAT:
323                 return row.getFloat(colName);
324             case DOUBLE:
325                 return row.getDouble(colName);
326             case BOOLEAN:
327                 return row.getBool(colName);
328             case MAP:
329                 return row.getMap(colName, String.class, String.class);
330             case LIST:
331                 return row.getList(colName, String.class);
332             default:
333                 return null;
334         }
335     }
336
337     public byte[] getBlobValue(Row row, String colName, DataType colType) {
338         ByteBuffer bb = row.getBytes(colName);
339         return bb.array();
340     }
341
342     public boolean doesRowSatisfyCondition(Row row, Map<String, Object> condition) throws Exception {
343         ColumnDefinitions colInfo = row.getColumnDefinitions();
344
345         for (Map.Entry<String, Object> entry : condition.entrySet()) {
346             String colName = entry.getKey();
347             DataType colType = colInfo.getType(colName);
348             Object columnValue = getColValue(row, colName, colType);
349             Object conditionValue = MusicUtil.convertToActualDataType(colType, entry.getValue());
350             if (columnValue.equals(conditionValue) == false)
351                 return false;
352         }
353         return true;
354     }
355
356     /**
357      * Utility function to store ResultSet values in to a MAP for output.
358      *
359      * @param results
360      * @return MAP
361      */
362     public Map<String, HashMap<String, Object>> marshalData(ResultSet results) {
363         Map<String, HashMap<String, Object>> resultMap =
364                         new HashMap<>();
365         int counter = 0;
366         for (Row row : results) {
367             ColumnDefinitions colInfo = row.getColumnDefinitions();
368             HashMap<String, Object> resultOutput = new HashMap<>();
369             for (Definition definition : colInfo) {
370                 if (!(("vector_ts").equals(definition.getName()))) {
371                     if(definition.getType().toString().toLowerCase().contains("blob")) {
372                         resultOutput.put(definition.getName(),
373                                 getBlobValue(row, definition.getName(), definition.getType()));
374                     } else {
375                         resultOutput.put(definition.getName(),
376                                     getColValue(row, definition.getName(), definition.getType()));
377                     }
378                 }
379             }
380             resultMap.put("row " + counter, resultOutput);
381             counter++;
382         }
383         return resultMap;
384     }
385
386
387     // Prepared Statements 1802 additions
388     
389     public boolean executePut(PreparedQueryObject queryObject, String consistency)
390             throws MusicServiceException, MusicQueryException {
391         return executePut(queryObject, consistency, 0);
392     }
393     /**
394      * This Method performs DDL and DML operations on Cassandra using specified consistency level
395      *
396      * @param queryObject Object containing cassandra prepared query and values.
397      * @param consistency Specify consistency level for data synchronization across cassandra
398      *        replicas
399      * @return Boolean Indicates operation success or failure
400      * @throws MusicServiceException
401      * @throws MusicQueryException
402      */
403     public boolean executePut(PreparedQueryObject queryObject, String consistency,long timeSlot)
404                     throws MusicServiceException, MusicQueryException {
405
406         boolean result = false;
407         long timeOfWrite = System.currentTimeMillis();
408         if (!MusicUtil.isValidQueryObject(!queryObject.getValues().isEmpty(), queryObject)) {
409             logger.error(EELFLoggerDelegate.errorLogger, queryObject.getQuery(),AppMessages.QUERYERROR, ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
410             throw new MusicQueryException("Ill formed queryObject for the request = " + "["
411                             + queryObject.getQuery() + "]");
412         }
413         logger.info(EELFLoggerDelegate.applicationLogger,
414                         "In preprared Execute Put: the actual insert query:"
415                                         + queryObject.getQuery() + "; the values"
416                                         + queryObject.getValues());
417         SimpleStatement preparedInsert = null;
418
419         try {
420             preparedInsert = new SimpleStatement(queryObject.getQuery(), queryObject.getValues().toArray());
421             if (consistency.equalsIgnoreCase(MusicUtil.CRITICAL)) {
422                 logger.info(EELFLoggerDelegate.applicationLogger, "Executing critical put query");
423                 preparedInsert.setConsistencyLevel(ConsistencyLevel.QUORUM);
424             } else if (consistency.equalsIgnoreCase(MusicUtil.EVENTUAL)) {
425                 logger.info(EELFLoggerDelegate.applicationLogger, "Executing simple put query");
426                 if(queryObject.getConsistency() == null)
427                     preparedInsert.setConsistencyLevel(ConsistencyLevel.ONE);
428                 else
429                     preparedInsert.setConsistencyLevel(MusicUtil.getConsistencyLevel(queryObject.getConsistency()));
430             } else if (consistency.equalsIgnoreCase(MusicUtil.ONE)) {
431                 preparedInsert.setConsistencyLevel(ConsistencyLevel.ONE);
432             }  else if (consistency.equalsIgnoreCase(MusicUtil.QUORUM)) {
433                 preparedInsert.setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM);
434             } else if (consistency.equalsIgnoreCase(MusicUtil.ALL)) {
435                 preparedInsert.setConsistencyLevel(ConsistencyLevel.ALL);
436             }
437             long timestamp = MusicUtil.v2sTimeStampInMicroseconds(timeSlot, timeOfWrite);
438             preparedInsert.setDefaultTimestamp(timestamp);
439
440             ResultSet rs = session.execute(preparedInsert);
441             result = rs.wasApplied();
442
443         }
444         catch (AlreadyExistsException ae) {
445             logger.error(EELFLoggerDelegate.errorLogger, ae.getMessage(),AppMessages.SESSIONFAILED+ " [" +
446                 queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR, ae);
447             throw new MusicServiceException(ae.getMessage());
448         }
449         catch (Exception e) {
450             logger.error(EELFLoggerDelegate.errorLogger, e.getMessage(),AppMessages.SESSIONFAILED + " [" 
451                 + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR, e);
452             throw new MusicServiceException("Executing Session Failure for Request = " + "["
453                             + queryObject.getQuery() + "]" + " Reason = " + e.getMessage());
454         }
455
456
457         return result;
458     }
459
460  /*   *//**
461      * This method performs DDL operations on Cassandra using consistency level ONE.
462      *
463      * @param queryObject Object containing cassandra prepared query and values.
464      * @return ResultSet
465      * @throws MusicServiceException
466      * @throws MusicQueryException
467      *//*
468     public ResultSet executeEventualGet(PreparedQueryObject queryObject)
469                     throws MusicServiceException, MusicQueryException {
470         CacheAccess<String, PreparedStatement> queryBank = CachingUtil.getStatementBank();
471         PreparedStatement preparedEventualGet = null;
472         if (!MusicUtil.isValidQueryObject(!queryObject.getValues().isEmpty(), queryObject)) {
473             logger.error(EELFLoggerDelegate.errorLogger, "",AppMessages.QUERYERROR+ " [" + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
474             throw new MusicQueryException("Ill formed queryObject for the request = " + "["
475                             + queryObject.getQuery() + "]");
476         }
477         logger.info(EELFLoggerDelegate.applicationLogger,
478                         "Executing Eventual  get query:" + queryObject.getQuery());
479
480         ResultSet results = null;
481         try {
482             if(queryBank.get(queryObject.getQuery()) != null )
483                 preparedEventualGet=queryBank.get(queryObject.getQuery());
484             else {
485                 preparedEventualGet = session.prepare(queryObject.getQuery());
486                 CachingUtil.updateStatementBank(queryObject.getQuery(), preparedEventualGet);
487             }
488             if(queryObject.getConsistency() == null) {
489                 preparedEventualGet.setConsistencyLevel(ConsistencyLevel.ONE);
490             } else {
491                 preparedEventualGet.setConsistencyLevel(MusicUtil.getConsistencyLevel(queryObject.getConsistency()));
492             }
493             results = session.execute(preparedEventualGet.bind(queryObject.getValues().toArray()));
494
495         } catch (Exception ex) {
496             logger.error("Exception", ex);
497             logger.error(EELFLoggerDelegate.errorLogger, ex.getMessage(),AppMessages.UNKNOWNERROR+ "[" + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
498             throw new MusicServiceException(ex.getMessage());
499         }
500         return results;
501     }
502
503     *//**
504      *
505      * This method performs DDL operation on Cassandra using consistency level QUORUM.
506      *
507      * @param queryObject Object containing cassandra prepared query and values.
508      * @return ResultSet
509      * @throws MusicServiceException
510      * @throws MusicQueryException
511      *//*
512     public ResultSet executeCriticalGet(PreparedQueryObject queryObject)
513                     throws MusicServiceException, MusicQueryException {
514         if (!MusicUtil.isValidQueryObject(!queryObject.getValues().isEmpty(), queryObject)) {
515             logger.error(EELFLoggerDelegate.errorLogger, "",AppMessages.QUERYERROR+ " [" + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
516             throw new MusicQueryException("Error processing Prepared Query Object for the request = " + "["
517                             + queryObject.getQuery() + "]");
518         }
519         logger.info(EELFLoggerDelegate.applicationLogger,
520                         "Executing Critical get query:" + queryObject.getQuery());
521         PreparedStatement preparedEventualGet = session.prepare(queryObject.getQuery());
522         preparedEventualGet.setConsistencyLevel(ConsistencyLevel.QUORUM);
523         ResultSet results = null;
524         try {
525             results = session.execute(preparedEventualGet.bind(queryObject.getValues().toArray()));
526         } catch (Exception ex) {
527             logger.error("Exception", ex);
528             logger.error(EELFLoggerDelegate.errorLogger, ex.getMessage(),AppMessages.UNKNOWNERROR+ "[" + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
529             throw new MusicServiceException(ex.getMessage());
530         }
531         return results;
532
533     }
534     */
535     public ResultSet executeGet(PreparedQueryObject queryObject,String consistencyLevel) throws MusicQueryException, MusicServiceException {
536         if (!MusicUtil.isValidQueryObject(!queryObject.getValues().isEmpty(), queryObject)) {
537             logger.error(EELFLoggerDelegate.errorLogger, "",AppMessages.QUERYERROR+ " [" + queryObject.getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR);
538             throw new MusicQueryException("Error processing Prepared Query Object for the request = " + "["
539                             + queryObject.getQuery() + "]");
540         }
541         ResultSet results = null;
542         try {
543             SimpleStatement statement = new SimpleStatement(queryObject.getQuery(), queryObject.getValues().toArray());
544
545             if (consistencyLevel.equalsIgnoreCase(CONSISTENCY_LEVEL_ONE)) {
546                 if(queryObject.getConsistency() == null) {
547                     statement.setConsistencyLevel(ConsistencyLevel.ONE);
548                 } else {
549                     statement.setConsistencyLevel(MusicUtil.getConsistencyLevel(queryObject.getConsistency()));
550                 }
551             }
552             else if (consistencyLevel.equalsIgnoreCase(CONSISTENCY_LEVEL_QUORUM)) {
553                 statement.setConsistencyLevel(ConsistencyLevel.QUORUM);
554             }
555
556             results = session.execute(statement);
557
558         } catch (Exception ex) {
559             logger.error(EELFLoggerDelegate.errorLogger, ex.getMessage(),AppMessages.UNKNOWNERROR+ "[" + queryObject
560                 .getQuery() + "]", ErrorSeverity.ERROR, ErrorTypes.QUERYERROR, ex);
561             throw new MusicServiceException(ex.getMessage());
562         }
563         
564         return results;
565         
566     }
567     
568     /**
569      * This method performs DDL operations on Cassandra using consistency level ONE.
570      * 
571      * @param queryObject Object containing cassandra prepared query and values.
572      */
573     public ResultSet executeOneConsistencyGet(PreparedQueryObject queryObject)
574                     throws MusicServiceException, MusicQueryException {
575         return executeGet(queryObject, CONSISTENCY_LEVEL_ONE);
576     }
577
578     /**
579      * 
580      * This method performs DDL operation on Cassandra using consistency level QUORUM.
581      * 
582      * @param queryObject Object containing cassandra prepared query and values.
583      */
584     public ResultSet executeQuorumConsistencyGet(PreparedQueryObject queryObject)
585                     throws MusicServiceException, MusicQueryException {
586         return executeGet(queryObject, CONSISTENCY_LEVEL_QUORUM);
587     }
588
589 }