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