Remove redundant type declarations
[mdsal.git] / blueprint / mdsal-blueprint-binding / src / main / java / org / opendaylight / controller / blueprint / ext / DataStoreAppConfigMetadata.java
1 /*
2  * Copyright (c) 2016 Brocade Communications 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.blueprint.ext;
9
10 import com.google.common.util.concurrent.FutureCallback;
11 import com.google.common.util.concurrent.MoreExecutors;
12 import java.io.File;
13 import java.io.IOException;
14 import java.net.URISyntaxException;
15 import java.util.Collection;
16 import java.util.Objects;
17 import java.util.Optional;
18 import java.util.concurrent.atomic.AtomicBoolean;
19 import javax.annotation.Nonnull;
20 import javax.annotation.Nullable;
21 import javax.xml.parsers.ParserConfigurationException;
22 import javax.xml.stream.XMLStreamException;
23 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
24 import org.opendaylight.controller.blueprint.ext.DataStoreAppConfigDefaultXMLReader.ConfigURLProvider;
25 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
26 import org.opendaylight.mdsal.binding.api.DataBroker;
27 import org.opendaylight.mdsal.binding.api.DataObjectModification;
28 import org.opendaylight.mdsal.binding.api.DataObjectModification.ModificationType;
29 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
30 import org.opendaylight.mdsal.binding.api.DataTreeModification;
31 import org.opendaylight.mdsal.binding.api.ReadTransaction;
32 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
33 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
34 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
35 import org.opendaylight.yangtools.concepts.ListenerRegistration;
36 import org.opendaylight.yangtools.yang.binding.DataObject;
37 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
38 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.osgi.service.blueprint.container.ComponentDefinitionException;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43 import org.w3c.dom.Element;
44 import org.xml.sax.SAXException;
45
46 /**
47  * Factory metadata corresponding to the "clustered-app-config" element that obtains an application's
48  * config data from the data store and provides the binding DataObject instance to the Blueprint container
49  * as a bean. In addition registers a DataTreeChangeListener to restart the Blueprint container when the
50  * config data is changed.
51  *
52  * @author Thomas Pantelis
53  */
54 public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactoryMetadata {
55     private static final Logger LOG = LoggerFactory.getLogger(DataStoreAppConfigMetadata.class);
56
57     static final String BINDING_CLASS = "binding-class";
58     static final String DEFAULT_CONFIG = "default-config";
59     static final String DEFAULT_CONFIG_FILE_NAME = "default-config-file-name";
60     static final String LIST_KEY_VALUE = "list-key-value";
61
62     private static final String DEFAULT_APP_CONFIG_FILE_PATH = "etc" + File.separator + "opendaylight" + File.separator
63             + "datastore" + File.separator + "initial" + File.separator + "config";
64
65     private final Element defaultAppConfigElement;
66     private final String defaultAppConfigFileName;
67     private final String appConfigBindingClassName;
68     private final String appConfigListKeyValue;
69     private final UpdateStrategy appConfigUpdateStrategy;
70     private final AtomicBoolean readingInitialAppConfig = new AtomicBoolean(true);
71
72     private volatile BindingContext bindingContext;
73     private volatile ListenerRegistration<?> appConfigChangeListenerReg;
74     private volatile DataObject currentAppConfig;
75
76     // Note: the BindingNormalizedNodeSerializer interface is annotated as deprecated because there's an
77     // equivalent interface in the mdsal project but the corresponding binding classes in the controller
78     // project are still used - conversion to the mdsal binding classes hasn't occurred yet.
79     private volatile BindingNormalizedNodeSerializer bindingSerializer;
80
81     public DataStoreAppConfigMetadata(@Nonnull final String id, @Nonnull final String appConfigBindingClassName,
82             @Nullable final String appConfigListKeyValue, @Nullable final String defaultAppConfigFileName,
83             @Nonnull final UpdateStrategy updateStrategyValue, @Nullable final Element defaultAppConfigElement) {
84         super(id);
85         this.defaultAppConfigElement = defaultAppConfigElement;
86         this.defaultAppConfigFileName = defaultAppConfigFileName;
87         this.appConfigBindingClassName = appConfigBindingClassName;
88         this.appConfigListKeyValue = appConfigListKeyValue;
89         this.appConfigUpdateStrategy = updateStrategyValue;
90     }
91
92     @Override
93     @SuppressWarnings("unchecked")
94     public void init(final ExtendedBlueprintContainer container) {
95         super.init(container);
96
97         Class<DataObject> appConfigBindingClass;
98         try {
99             Class<?> bindingClass = container.getBundleContext().getBundle().loadClass(appConfigBindingClassName);
100             if (!DataObject.class.isAssignableFrom(bindingClass)) {
101                 throw new ComponentDefinitionException(String.format(
102                         "%s: Specified app config binding class %s does not extend %s",
103                         logName(), appConfigBindingClassName, DataObject.class.getName()));
104             }
105
106             appConfigBindingClass = (Class<DataObject>) bindingClass;
107         } catch (final ClassNotFoundException e) {
108             throw new ComponentDefinitionException(String.format("%s: Error loading app config binding class %s",
109                     logName(), appConfigBindingClassName), e);
110         }
111
112         bindingContext = BindingContext.create(logName(), appConfigBindingClass, appConfigListKeyValue);
113     }
114
115     @Override
116     public Object create() throws ComponentDefinitionException {
117         LOG.debug("{}: In create - currentAppConfig: {}", logName(), currentAppConfig);
118
119         super.onCreate();
120
121         return currentAppConfig;
122     }
123
124     @Override
125     protected void startTracking() {
126         // First get the BindingNormalizedNodeSerializer OSGi service. This will be used to create a default
127         // instance of the app config binding class, if necessary.
128
129         retrieveService("binding-codec", BindingNormalizedNodeSerializer.class, service -> {
130             bindingSerializer = (BindingNormalizedNodeSerializer)service;
131             retrieveDataBrokerService();
132         });
133     }
134
135     private void retrieveDataBrokerService() {
136         LOG.debug("{}: In retrieveDataBrokerService", logName());
137         // Get the binding DataBroker OSGi service.
138         retrieveService("data-broker", DataBroker.class, service -> retrieveInitialAppConfig((DataBroker)service));
139     }
140
141     private void retrieveInitialAppConfig(final DataBroker dataBroker) {
142         LOG.debug("{}: Got DataBroker instance - reading app config {}", logName(), bindingContext.appConfigPath);
143
144         setDependencyDesc("Initial app config " + bindingContext.appConfigBindingClass.getSimpleName());
145
146         // We register a DTCL to get updates and also read the app config data from the data store. If
147         // the app config data is present then both the read and initial DTCN update will return it. If the
148         // the data isn't present, we won't get an initial DTCN update so the read will indicate the data
149         // isn't present.
150
151         DataTreeIdentifier<DataObject> dataTreeId = DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
152                 bindingContext.appConfigPath);
153         appConfigChangeListenerReg = dataBroker.registerDataTreeChangeListener(dataTreeId,
154                 (ClusteredDataTreeChangeListener<DataObject>) this::onAppConfigChanged);
155
156         readInitialAppConfig(dataBroker);
157     }
158
159     private void readInitialAppConfig(final DataBroker dataBroker) {
160         try (ReadTransaction readOnlyTx = dataBroker.newReadOnlyTransaction()) {
161             readOnlyTx.read(LogicalDatastoreType.CONFIGURATION, bindingContext.appConfigPath).addCallback(
162                 new FutureCallback<Optional<DataObject>>() {
163                     @Override
164                     public void onSuccess(final Optional<DataObject> possibleAppConfig) {
165                         LOG.debug("{}: Read of app config {} succeeded: {}", logName(), bindingContext
166                             .appConfigBindingClass.getName(), possibleAppConfig);
167                         setInitialAppConfig(possibleAppConfig);
168                     }
169
170                     @Override
171                     public void onFailure(final Throwable failure) {
172                         // We may have gotten the app config via the data tree change listener so only retry if not.
173                         if (readingInitialAppConfig.get()) {
174                             LOG.warn("{}: Read of app config {} failed - retrying", logName(),
175                                 bindingContext.appConfigBindingClass.getName(), failure);
176
177                             readInitialAppConfig(dataBroker);
178                         }
179                     }
180                 }, MoreExecutors.directExecutor());
181         }
182     }
183
184     private void onAppConfigChanged(final Collection<DataTreeModification<DataObject>> changes) {
185         for (DataTreeModification<DataObject> change: changes) {
186             DataObjectModification<DataObject> changeRoot = change.getRootNode();
187             ModificationType type = changeRoot.getModificationType();
188
189             LOG.debug("{}: onAppConfigChanged: {}, {}", logName(), type, change.getRootPath());
190
191             if (type == ModificationType.SUBTREE_MODIFIED || type == ModificationType.WRITE) {
192                 DataObject newAppConfig = changeRoot.getDataAfter();
193
194                 LOG.debug("New app config instance: {}, previous: {}", newAppConfig, currentAppConfig);
195
196                 if (!setInitialAppConfig(Optional.of(newAppConfig))
197                         && !Objects.equals(currentAppConfig, newAppConfig)) {
198                     LOG.debug("App config was updated");
199
200                     if (appConfigUpdateStrategy == UpdateStrategy.RELOAD) {
201                         restartContainer();
202                     }
203                 }
204             } else if (type == ModificationType.DELETE) {
205                 LOG.debug("App config was deleted");
206
207                 if (appConfigUpdateStrategy == UpdateStrategy.RELOAD) {
208                     restartContainer();
209                 }
210             }
211         }
212     }
213
214     private boolean setInitialAppConfig(final Optional<DataObject> possibleAppConfig) {
215         boolean result = readingInitialAppConfig.compareAndSet(true, false);
216         if (result) {
217             DataObject localAppConfig;
218             if (possibleAppConfig.isPresent()) {
219                 localAppConfig = possibleAppConfig.get();
220             } else {
221                 // No app config data is present so create an empty instance via the bindingSerializer service.
222                 // This will also return default values for leafs that haven't been explicitly set.
223                 localAppConfig = createDefaultInstance();
224             }
225
226             LOG.debug("{}: Setting currentAppConfig instance: {}", logName(), localAppConfig);
227
228             // Now publish the app config instance to the volatile field and notify the callback to let the
229             // container know our dependency is now satisfied.
230             currentAppConfig = localAppConfig;
231             setSatisfied();
232         }
233
234         return result;
235     }
236
237     private DataObject createDefaultInstance() {
238         try {
239             ConfigURLProvider inputStreamProvider = appConfigFileName -> {
240                 File appConfigFile = new File(DEFAULT_APP_CONFIG_FILE_PATH, appConfigFileName);
241                 LOG.debug("{}: parsePossibleDefaultAppConfigXMLFile looking for file {}", logName(),
242                         appConfigFile.getAbsolutePath());
243
244                 if (!appConfigFile.exists()) {
245                     return Optional.empty();
246                 }
247
248                 LOG.debug("{}: Found file {}", logName(), appConfigFile.getAbsolutePath());
249
250                 return Optional.of(appConfigFile.toURI().toURL());
251             };
252
253             DataStoreAppConfigDefaultXMLReader<?> reader = new DataStoreAppConfigDefaultXMLReader<>(logName(),
254                     defaultAppConfigFileName, getOSGiService(DOMSchemaService.class), bindingSerializer, bindingContext,
255                     inputStreamProvider);
256             return reader.createDefaultInstance((schemaContext, dataSchema) -> {
257                 // Fallback if file cannot be read, try XML from Config
258                 NormalizedNode<?, ?> dataNode = parsePossibleDefaultAppConfigElement(schemaContext, dataSchema);
259                 if (dataNode != null) {
260                     return dataNode;
261                 }
262                 // or, as last resort, defaults from the model
263                 return bindingContext.newDefaultNode(dataSchema);
264             });
265
266         } catch (final ConfigXMLReaderException | IOException | SAXException | XMLStreamException
267                 | ParserConfigurationException | URISyntaxException e) {
268             if (e.getCause() == null) {
269                 setFailureMessage(e.getMessage());
270             } else {
271                 setFailure(e.getMessage(), e);
272             }
273             return null;
274         }
275     }
276
277     @Nullable
278     private NormalizedNode<?, ?> parsePossibleDefaultAppConfigElement(final SchemaContext schemaContext,
279             final DataSchemaNode dataSchema) throws URISyntaxException, IOException, ParserConfigurationException,
280             SAXException, XMLStreamException {
281         if (defaultAppConfigElement == null) {
282             return null;
283         }
284
285         LOG.debug("{}: parsePossibleDefaultAppConfigElement for {}", logName(), bindingContext.bindingQName);
286
287         LOG.debug("{}: Got app config schema: {}", logName(), dataSchema);
288
289         NormalizedNode<?, ?> dataNode = bindingContext.parseDataElement(defaultAppConfigElement, dataSchema,
290                 schemaContext);
291
292         LOG.debug("{}: Parsed data node: {}", logName(), dataNode);
293
294         return dataNode;
295     }
296
297     @Override
298     public void destroy(final Object instance) {
299         super.destroy(instance);
300
301         if (appConfigChangeListenerReg != null) {
302             appConfigChangeListenerReg.close();
303             appConfigChangeListenerReg = null;
304         }
305     }
306
307 }