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