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