BUG-997: make sure we carry YANG text around
[yangtools.git] / yang / yang-parser-impl / src / main / java / org / opendaylight / yangtools / yang / parser / repo / URLSchemaContextResolver.java
1 /*
2  * Copyright (c) 2014 Cisco 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.yangtools.yang.parser.repo;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import com.google.common.base.Function;
13 import com.google.common.base.Objects.ToStringHelper;
14 import com.google.common.base.Optional;
15 import com.google.common.base.Preconditions;
16 import com.google.common.collect.ArrayListMultimap;
17 import com.google.common.collect.ImmutableMultimap;
18 import com.google.common.collect.Maps;
19 import com.google.common.collect.Maps.EntryTransformer;
20 import com.google.common.collect.Multimap;
21 import com.google.common.collect.Multimaps;
22 import com.google.common.util.concurrent.CheckedFuture;
23 import com.google.common.util.concurrent.FutureCallback;
24 import com.google.common.util.concurrent.Futures;
25 import com.google.common.util.concurrent.ListeningExecutorService;
26 import com.google.common.util.concurrent.MoreExecutors;
27 import com.google.common.util.concurrent.ThreadFactoryBuilder;
28
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.net.URL;
32 import java.util.Collection;
33 import java.util.LinkedHashMap;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 import java.util.Queue;
37 import java.util.concurrent.ConcurrentLinkedQueue;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.ThreadFactory;
40 import java.util.concurrent.atomic.AtomicReference;
41
42 import javax.annotation.concurrent.GuardedBy;
43 import javax.annotation.concurrent.ThreadSafe;
44
45 import org.antlr.v4.runtime.ParserRuleContext;
46 import org.antlr.v4.runtime.tree.ParseTreeWalker;
47 import org.opendaylight.yangtools.concepts.AbstractObjectRegistration;
48 import org.opendaylight.yangtools.concepts.ObjectRegistration;
49 import org.opendaylight.yangtools.yang.model.api.Module;
50 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
51 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
52 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
53 import org.opendaylight.yangtools.yang.model.repo.spi.SchemaSourceTransformationException;
54 import org.opendaylight.yangtools.yang.parser.builder.impl.ModuleBuilder;
55 import org.opendaylight.yangtools.yang.parser.impl.YangParserImpl;
56 import org.opendaylight.yangtools.yang.parser.impl.YangParserListenerImpl;
57 import org.opendaylight.yangtools.yang.parser.impl.util.YangModelDependencyInfo;
58 import org.opendaylight.yangtools.yang.parser.util.ASTSchemaSource;
59 import org.opendaylight.yangtools.yang.parser.util.TextToASTTransformer;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 @ThreadSafe
64 public class URLSchemaContextResolver {
65     private static final Logger LOG = LoggerFactory.getLogger(URLSchemaContextResolver.class);
66     private static final Function<ASTSchemaSource, YangModelDependencyInfo> EXTRACT_DEPINFO = new Function<ASTSchemaSource, YangModelDependencyInfo>() {
67         @Override
68         public YangModelDependencyInfo apply(final ASTSchemaSource input) {
69             return input.getDependencyInformation();
70         }
71     };
72     private static final EntryTransformer<SourceIdentifier, Collection<YangModelDependencyInfo>, YangModelDependencyInfo> SQUASH_DEPINFO =
73             new EntryTransformer<SourceIdentifier, Collection<YangModelDependencyInfo>, YangModelDependencyInfo>() {
74         @Override
75         public YangModelDependencyInfo transformEntry(final SourceIdentifier key, final Collection<YangModelDependencyInfo> value) {
76             // FIXME: validate that all the info objects are the same
77             return value.iterator().next();
78         }
79     };
80     private static final Function<ASTSchemaSource, ParserRuleContext> EXTRACT_AST = new Function<ASTSchemaSource, ParserRuleContext>() {
81         @Override
82         public ParserRuleContext apply(final ASTSchemaSource input) {
83             return input.getAST();
84         }
85     };
86     private static final EntryTransformer<SourceIdentifier, Collection<ParserRuleContext>, ParserRuleContext> SQUASH_AST =
87             new EntryTransformer<SourceIdentifier, Collection<ParserRuleContext>, ParserRuleContext>() {
88         @Override
89         public ParserRuleContext transformEntry(final SourceIdentifier key, final Collection<ParserRuleContext> value) {
90             // FIXME: validate that all the info objects are the same
91             return value.iterator().next();
92         }
93     };
94
95     @GuardedBy("this")
96     private final Multimap<SourceIdentifier, ASTSchemaSource> resolvedRegs = ArrayListMultimap.create();
97     private final AtomicReference<Optional<SchemaContext>> currentSchemaContext = new AtomicReference<>(Optional.<SchemaContext>absent());
98     private final Queue<URLRegistration> outstandingRegs = new ConcurrentLinkedQueue<>();
99     private final TextToASTTransformer transformer;
100     @GuardedBy("this")
101     private Object version = new Object();
102     @GuardedBy("this")
103     private Object contextVersion = version;
104
105     private final class URLRegistration extends AbstractObjectRegistration<URL> {
106         @GuardedBy("this")
107         private CheckedFuture<ASTSchemaSource, SchemaSourceTransformationException> future;
108         @GuardedBy("this")
109         private ASTSchemaSource result;
110
111         protected URLRegistration(final URL url, final CheckedFuture<ASTSchemaSource, SchemaSourceTransformationException> future) {
112             super(url);
113             this.future = Preconditions.checkNotNull(future);
114         }
115
116         private synchronized boolean setResult(final ASTSchemaSource result) {
117             if (future != null) {
118                 this.result = result;
119                 return true;
120             } else {
121                 return false;
122             }
123         }
124
125         @Override
126         protected void removeRegistration() {
127             // Cancel the future, but it may already be completing
128             future.cancel(false);
129
130             synchronized (this) {
131                 future = null;
132                 outstandingRegs.remove(this);
133                 if (result != null) {
134                     removeSchemaSource(result);
135                 }
136             }
137         }
138     }
139
140     private URLSchemaContextResolver(final TextToASTTransformer transformer) {
141         this.transformer = Preconditions.checkNotNull(transformer);
142     }
143
144     public static URLSchemaContextResolver create(final String name) {
145         final ThreadFactory f = new ThreadFactoryBuilder().setDaemon(true).setNameFormat(name + "yangparser-%d").build();
146         final ListeningExecutorService s = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(f));
147
148         return new URLSchemaContextResolver(TextToASTTransformer.create(s));
149     }
150
151     /**
152      * Register a URL hosting a YANG Text file.
153      *
154      * @param url URL
155      */
156     public ObjectRegistration<URL> registerSource(final URL url) {
157         checkArgument(url != null, "Supplied URL must not be null");
158
159         final SourceIdentifier id = SourceIdentifier.create(url.getFile().toString(), Optional.<String>absent());
160         final YangTextSchemaSource text = new YangTextSchemaSource(id) {
161             @Override
162             public InputStream openStream() throws IOException {
163                 return url.openStream();
164             }
165
166             @Override
167             protected ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
168                 return toStringHelper.add("url", url);
169             }
170         };
171
172         final CheckedFuture<ASTSchemaSource, SchemaSourceTransformationException> ast = transformer.transformSchemaSource(text);
173         final URLRegistration reg = new URLRegistration(url, ast);
174         outstandingRegs.add(reg);
175
176         Futures.addCallback(ast, new FutureCallback<ASTSchemaSource>() {
177             @Override
178             public void onSuccess(final ASTSchemaSource result) {
179                 LOG.trace("Resolved URL {} to source {}", url, result);
180
181                 outstandingRegs.remove(reg);
182                 if (reg.setResult(result)) {
183                     addSchemaSource(result);
184                 }
185             }
186
187             @Override
188             public void onFailure(final Throwable t) {
189                 LOG.warn("Failed to parse YANG text from {}, ignoring it", url, t);
190                 outstandingRegs.remove(reg);
191             }
192         });
193
194         return reg;
195     }
196
197     private synchronized void addSchemaSource(final ASTSchemaSource src) {
198         resolvedRegs.put(src.getIdentifier(), src);
199         version = new Object();
200     }
201
202     private synchronized void removeSchemaSource(final ASTSchemaSource src) {
203         resolvedRegs.put(src.getIdentifier(), src);
204         version = new Object();
205     }
206
207     /**
208      * Try to parse all currently available yang files and build new schema context.
209      * @return new schema context iif there is at least 1 yang file registered and
210      *         new schema context was successfully built.
211      */
212     public Optional<SchemaContext> getSchemaContext() {
213         while (true) {
214             Optional<SchemaContext> result;
215             final Multimap<SourceIdentifier, ASTSchemaSource> sources;
216             final Object v;
217             synchronized (this) {
218                 result = currentSchemaContext.get();
219                 if (version == contextVersion) {
220                     return result;
221                 }
222
223                 sources = ImmutableMultimap.copyOf(resolvedRegs);
224                 v = version;
225             }
226
227             if (!sources.isEmpty()) {
228                 final Map<SourceIdentifier, YangModelDependencyInfo> deps =
229                         Maps.transformEntries(Multimaps.transformValues(sources, EXTRACT_DEPINFO).asMap(), SQUASH_DEPINFO);
230
231                 LOG.debug("Resolving dependency reactor {}", deps);
232                 final DependencyResolver res = DependencyResolver.create(deps);
233                 if (!res.getUnresolvedSources().isEmpty()) {
234                     LOG.debug("Omitting models {} due to unsatisfied imports {}", res.getUnresolvedSources(), res.getUnsatisfiedImports());
235                 }
236
237                 final Map<SourceIdentifier, ParserRuleContext> asts =
238                         Maps.transformEntries(Multimaps.transformValues(sources, EXTRACT_AST).asMap(), SQUASH_AST);
239
240                 final ParseTreeWalker walker = new ParseTreeWalker();
241                 final Map<SourceIdentifier, ModuleBuilder> sourceToBuilder = new LinkedHashMap<>();
242
243                 for (Entry<SourceIdentifier, ParserRuleContext> entry : asts.entrySet()) {
244                     final YangParserListenerImpl yangModelParser = new YangParserListenerImpl(entry.getKey().getName());
245                     walker.walk(yangModelParser, entry.getValue());
246                     ModuleBuilder moduleBuilder = yangModelParser.getModuleBuilder();
247
248                     moduleBuilder.setSource(sources.get(entry.getKey()).iterator().next().getYangText());
249                     sourceToBuilder.put(entry.getKey(), moduleBuilder);
250                 }
251                 LOG.debug("Modules ready for integration");
252
253                 final YangParserImpl parser = YangParserImpl.getInstance();
254                 final Collection<Module> modules = parser.buildModules(sourceToBuilder.values());
255                 LOG.debug("Integrated cross-references modules");
256
257                 result = Optional.of(parser.assembleContext(modules));
258             } else {
259                 result = Optional.absent();
260             }
261
262             synchronized (this) {
263                 if (v == version) {
264                     currentSchemaContext.set(result);
265                     contextVersion = version;
266                     return result;
267                 }
268
269                 LOG.debug("Context version {} expected {}, retry", version, v);
270             }
271         }
272     }
273 }