bf1aff372d6acb224dbd9ecff8fd34c69b39b28d
[ccsdk/features.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP : ccsdk features
4  * ================================================================================
5  * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property.
6  * All rights reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  *
21  */
22 package org.onap.ccsdk.features.sdnr.wt.dataprovider.database;
23
24 import com.fasterxml.jackson.core.JsonProcessingException;
25 import java.io.IOException;
26 import java.lang.reflect.Field;
27 import java.util.List;
28 import javax.annotation.Nonnull;
29 import javax.annotation.Nullable;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.onap.ccsdk.features.sdnr.wt.common.database.DatabaseClient;
32 import org.onap.ccsdk.features.sdnr.wt.common.database.SearchHit;
33 import org.onap.ccsdk.features.sdnr.wt.common.database.SearchResult;
34 import org.onap.ccsdk.features.sdnr.wt.common.database.queries.QueryBuilder;
35 import org.onap.ccsdk.features.sdnr.wt.dataprovider.yangtools.YangToolsMapper2;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.data.provider.rev201110.Entity;
37 import org.opendaylight.yangtools.concepts.Builder;
38 import org.opendaylight.yangtools.yang.binding.DataObject;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * Class to rw yang-tool generated objects into elasticsearch database. For "ES _id" exchange the esIdAddAtributteName
44  * is used. This attribute mast be of type String and contains for read and write operations the object id. The function
45  * can be used without id handling. If id handling is required the parameter needs to be specified by class definition
46  * in yang and setting the name by using setAttributeName()
47  *
48  * Due to using Jackson base interfaces the org.eclipse.jdt.annotation.NonNull needs to be used here to get rid of
49  * warnings
50  *
51  * @param <T> Yang tools generated class object.
52  */
53 public class EsDataObjectReaderWriter2<T extends DataObject> {
54
55     private final Logger LOG = LoggerFactory.getLogger(EsDataObjectReaderWriter2.class);
56
57     /** Typename for elastic search data schema **/
58     private String dataTypeName;
59
60     /** Elasticsearch Database client to be used **/
61     private DatabaseClient db;
62
63     /** Mapper with configuration to use opendaylight yang-tools builder pattern for object creation **/
64     private YangToolsMapper2<T> yangtoolsMapper;
65
66     /** Class of T as attribute to allow JSON to Class object mapping **/
67     private Class<T> clazz;
68
69     /** Field is used to write id. If null no id handling **/
70     private @Nullable Field field;
71
72     /** Attribute that is used as id field for the database object **/
73     private @Nullable String esIdAddAtributteName;
74
75     /** Interface to be used for write operations. Rule for write: T extends S and **/
76     private Class<? extends DataObject> writeInterfaceClazz; // == "S"
77     /** Flag true to sync this attribute during write always, what is slow and false do not sync */
78     private final boolean syncAfterWrite;
79
80     /**
81      * Elasticsearch database read and write for specific class, defined by opendaylight yang-tools.
82      *
83      * @param <X>
84      * @param <B>
85      * @param db Database access client
86      * @param dataTypeName typename in database schema
87      * @param clazz class of type to be handled
88      * @param builderClazz class to build related object if builder pattern should be used.
89      * @param syncAfterWrite
90      * @throws ClassNotFoundException
91      */
92     public <X extends T, @NonNull B extends Builder<X>> EsDataObjectReaderWriter2(DatabaseClient db,
93             String dataTypeName, @Nonnull Class<T> clazz, @Nullable Class<B> builderClazz, boolean syncAfterWrite)
94             throws ClassNotFoundException {
95         LOG.info("Create {} for datatype {} class {}", this.getClass().getName(), dataTypeName, clazz.getName());
96
97         this.esIdAddAtributteName = null;
98         this.field = null;
99         this.writeInterfaceClazz = clazz;
100         this.db = db;
101         this.dataTypeName = dataTypeName;
102         this.yangtoolsMapper = new YangToolsMapper2<>(clazz, builderClazz);
103         this.clazz = clazz;
104         this.syncAfterWrite = syncAfterWrite;
105     }
106
107     public <X extends T, @NonNull B extends Builder<X>> EsDataObjectReaderWriter2(DatabaseClient db,
108             Entity dataTypeName, @Nonnull Class<T> clazz, @Nullable Class<B> builderClazz)
109             throws ClassNotFoundException {
110         this(db, dataTypeName.getName(), clazz, builderClazz, false);
111     }
112
113     public <X extends T, @NonNull B extends Builder<X>> EsDataObjectReaderWriter2(DatabaseClient db,
114             Entity dataTypeName, @Nonnull Class<T> clazz, @Nullable Class<B> builderClazz, boolean syncAfterWrite)
115             throws ClassNotFoundException {
116         this(db, dataTypeName.getName(), clazz, builderClazz, syncAfterWrite);
117     }
118
119     public EsDataObjectReaderWriter2(DatabaseClient db, Entity dataTypeName, @Nonnull Class<T> clazz,
120             boolean syncAfterWrite) throws ClassNotFoundException {
121         this(db, dataTypeName.getName(), clazz, null, syncAfterWrite);
122     }
123
124     public EsDataObjectReaderWriter2(DatabaseClient db, Entity dataTypeName, Class<T> clazz)
125             throws ClassNotFoundException {
126         this(db, dataTypeName.getName(), clazz, null, false);
127     }
128
129     /**
130      * Get Datatype name
131      *
132      * @return string with dataTypeName
133      */
134     public String getDataTypeName() {
135         return dataTypeName;
136     }
137
138     /**
139      * Simlar to {@link #setEsIdAttributeName()}, but adapts the parameter to yangtools attribute naming schema
140      *
141      * @param esIdAttributeName is converted to UnderscoreCamelCase
142      * @return this for further operations.
143      */
144     public EsDataObjectReaderWriter2<T> setEsIdAttributeNameCamelized(String esIdAttributeName) {
145         return setEsIdAttributeName(YangToolsMapper2.toCamelCaseAttributeName(esIdAttributeName));
146     }
147
148     /**
149      * Attribute name of class that is containing the object id
150      *
151      * @param esIdAttributeName of the implementation class for the yangtools interface. Expected attribute name format
152      *        is CamelCase with leading underline. @
153      * @return this for further operations.
154      * @throws SecurityException if no access or IllegalArgumentException if wrong type or no attribute with this name.
155      */
156     public EsDataObjectReaderWriter2<T> setEsIdAttributeName(String esIdAttributeName) {
157         LOG.debug("Set attribute '{}'", esIdAttributeName);
158         this.esIdAddAtributteName = null; // Reset status
159         this.field = null;
160
161         Field attributeField;
162         try {
163             Builder<? extends T> builder = yangtoolsMapper.getBuilder(clazz);
164             if (builder == null) {
165                 String msg = "No builder for " + clazz;
166                 LOG.debug(msg);
167                 throw new IllegalArgumentException(msg);
168             } else {
169                 T object = builder.build();
170                 attributeField = object.getClass().getDeclaredField(esIdAttributeName);
171                 if (attributeField.getType().equals(String.class)) {
172                     attributeField.setAccessible(true);
173                     this.esIdAddAtributteName = esIdAttributeName; // Set new status if everything OK
174                     this.field = attributeField;
175                 } else {
176                     String msg = "Wrong field type " + attributeField.getType().getName() + " of " + esIdAttributeName;
177                     LOG.debug(msg);
178                     throw new IllegalArgumentException(msg);
179                 }
180             }
181         } catch (NoSuchFieldException e) {
182             // Convert to run-time exception
183             String msg = "NoSuchFieldException for '" + esIdAttributeName + "' in class " + clazz.getName();
184             LOG.debug(msg);
185             throw new IllegalArgumentException(msg);
186         } catch (SecurityException e) {
187             LOG.debug("Access problem " + esIdAttributeName, e);
188             throw e;
189         }
190         return this;
191     }
192
193     /**
194      * Specify subclass of T for write operations.
195      *
196      * @param writeInterfaceClazz
197      */
198     public EsDataObjectReaderWriter2<T> setWriteInterface(@Nonnull Class<? extends DataObject> writeInterfaceClazz) {
199         LOG.debug("Set write interface to {}", writeInterfaceClazz);
200         if (writeInterfaceClazz == null) {
201             throw new IllegalArgumentException("Null not allowed here.");
202         }
203
204         this.writeInterfaceClazz = writeInterfaceClazz;
205         return this;
206     }
207
208     public interface IdGetter<S extends DataObject> {
209         String getId(S object);
210     }
211
212     public <S extends DataObject> void write(List<S> objectList, IdGetter<S> idGetter) {
213         for (S object : objectList) {
214             write(object, idGetter.getId(object));
215         }
216     }
217
218     /**
219      * Write child object to database with specific id
220      *
221      * @param object to be written
222      * @param esId use the id or if null generate unique id
223      * @return String with id or null
224      */
225     public @Nullable <S extends DataObject> String write(S object, @Nullable String esId) {
226         if (object != null && writeInterfaceClazz.isInstance(object)) {
227             try {
228                 String json = yangtoolsMapper.writeValueAsString(object);
229                 return db.doWriteRaw(dataTypeName, esId, json, this.syncAfterWrite);
230             } catch (JsonProcessingException e) {
231                 LOG.error("Write problem: ", e);
232             }
233         } else {
234             LOG.error("Type {} does not provide interface {}", object != null ? object.getClass().getName() : "null",
235                     writeInterfaceClazz.getName());
236         }
237         return null;
238     }
239
240     /**
241      * Update partial child object to database with match/term query
242      *
243      * @param <S> of object
244      * @param object to write
245      * @param query for write of specific attributes
246      * @return json string with new Object
247      */
248     public @Nullable <S extends DataObject> boolean update(S object, QueryBuilder query) {
249         if (object != null && writeInterfaceClazz.isInstance(object)) {
250             try {
251                 String json = yangtoolsMapper.writeValueAsString(object);
252                 return db.doUpdate(this.dataTypeName, json, query);
253             } catch (JsonProcessingException e) {
254                 LOG.error("Update problem: ", e);
255             }
256         } else {
257             LOG.error("Type {} does not provide interface {}", object != null ? object.getClass().getName() : "null",
258                     writeInterfaceClazz.getName());
259         }
260         return false;
261     }
262
263     /**
264      * Write/ update partial child object to database with specific id Write if not exists, else update
265      *
266      * @param object
267      * @param esId
268      * @return String with esId or null
269      */
270     public @Nullable <S extends DataObject> String update(S object, String esId) {
271         return this.updateOrCreate(object, esId, null);
272     }
273
274     /**
275      * See {@link doUpdateOrCreate(String dataTypeName, String esId, String json, List<String> doNotUpdateField) }
276      */
277     public @Nullable <S extends DataObject> String updateOrCreate(S object, String esId, List<String> onlyForInsert) {
278         if (object != null && writeInterfaceClazz.isInstance(object)) {
279             try {
280                 String json = yangtoolsMapper.writeValueAsString(object);
281                 return db.doUpdateOrCreate(dataTypeName, esId, json, onlyForInsert);
282             } catch (JsonProcessingException e) {
283                 LOG.error("Update problem: ", e);
284             }
285         } else {
286             LOG.error("Type {} does not provide interface {}", object != null ? object.getClass().getName() : "null",
287                     writeInterfaceClazz.getName());
288         }
289         return null;
290     }
291
292     /**
293      * Read object from database, by using the id field
294      *
295      * @param object
296      * @return
297      */
298     public @Nullable T read(String esId) {
299         @Nullable
300         T res = null;
301         if (esId != null) {
302             String json = db.doReadJsonData(dataTypeName, esId);
303             if (json != null) {
304                 try {
305                     res = yangtoolsMapper.readValue(json.getBytes(), clazz);
306                 } catch (IOException e) {
307                     LOG.error("Problem: ", e);
308                 }
309             } else {
310                 LOG.debug("Can not read from DB id {} type {}", esId, dataTypeName);
311             }
312         }
313         return res;
314     }
315
316     /**
317      * Remove object
318      *
319      * @param esId to identify the object.
320      * @return success
321      */
322     public boolean remove(String esId) {
323         return db.doRemove(this.dataTypeName, esId);
324     }
325
326     public int remove(QueryBuilder query) {
327         return this.db.doRemove(this.dataTypeName, query);
328     }
329
330     /**
331      * Get all elements of related type
332      *
333      * @return all Elements
334      */
335     public SearchResult<T> doReadAll() {
336         return doReadAll(null);
337     }
338
339     public SearchResult<T> doReadAll(QueryBuilder query) {
340         return this.doReadAll(query, false);
341     }
342
343     /**
344      * Read all existing objects of a type
345      *
346      * @param query for the elements
347      * @return the list of all objects
348      */
349
350     public SearchResult<T> doReadAll(QueryBuilder query, boolean ignoreException) {
351
352         SearchResult<T> res = new SearchResult<T>();
353
354         SearchResult<SearchHit> result;
355         List<SearchHit> hits;
356         if (query != null) {
357             LOG.debug("read data in {} with query {}", dataTypeName, query.toJSON());
358             result = db.doReadByQueryJsonData(dataTypeName, query, ignoreException);
359         } else {
360             result = db.doReadAllJsonData(dataTypeName, ignoreException);
361         }
362         hits = result.getHits();
363         LOG.debug("Read: {} elements: {}  Failures: {}", dataTypeName, hits.size(),
364                 yangtoolsMapper.getMappingFailures());
365
366         T object;
367         for (SearchHit hit : hits) {
368             object = getT(hit.getSourceAsString());
369             LOG.debug("Mapp Object: {}\nSource: '{}'\nResult: '{}'\n Failures: {}", hit.getId(),
370                     hit.getSourceAsString(), object, yangtoolsMapper.getMappingFailures());
371             if (object != null) {
372                 setEsId(object, hit.getId());
373                 res.add(object);
374             } else {
375                 LOG.warn("Mapp result null Object: {}\n Source: '{}'\n : '", hit.getId(), hit.getSourceAsString());
376             }
377         }
378         res.setTotal(result.getTotal());
379         return res;
380     }
381
382     /* ---------------------------------------------
383      * Private functions
384      */
385
386     private void setEsId(T object, String esId) {
387         if (field != null) {
388             try {
389                 field.set(object, esId);
390             } catch (IllegalArgumentException | IllegalAccessException e) {
391                 LOG.debug("Field set problem.", e);
392             }
393         }
394     }
395
396     private @Nullable T getT(String jsonString) {
397         try {
398             return yangtoolsMapper.readValue(jsonString, clazz);
399         } catch (IOException e) {
400             LOG.info("Mapping problem", e);
401             return null;
402         }
403     }
404
405 }