summaryrefslogtreecommitdiffstats
path: root/vpp-integration/api-docs/core/src
diff options
context:
space:
mode:
authorJan Srnicek <jsrnicek@cisco.com>2017-07-21 15:10:46 +0200
committerMarek Gradzki <mgradzki@cisco.com>2017-07-25 14:49:21 +0200
commit69f669f5d16768258f854ce4139033f94ab2afb9 (patch)
tree4e427e692f0cdaf3e0a412f26279aeef1895fa69 /vpp-integration/api-docs/core/src
parent25666a0bf99f7b4cf15f14000015014098ddbb6c (diff)
HC2VPP-180 - Doc coverage generator
TODO - links to specific vpp api section(now points just to section with apis) TODO - links to specific java binding code(now points to class thats doing binding) TODO - operational coverage(ASM does not support lambda processing) TODO - generate coverage adoc links Change-Id: I44c85012da3bd2e7cdd41930753e5aae6955cd7b Signed-off-by: Jan Srnicek <jsrnicek@cisco.com> Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
Diffstat (limited to 'vpp-integration/api-docs/core/src')
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ClassPathTypeIndex.java74
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CollectingWriterBuilder.java153
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageGenerator.java184
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageScanner.java74
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/LinkGenerator.java33
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodDelegatingClassVisitor.java54
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodPluginCoverageVisitor.java90
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelLinkIndex.java71
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelTypeIndex.java88
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/PluginMethodReference.java56
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/VppApiUtils.java43
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangModelKey.java73
-rw-r--r--vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangTypeLinkIndex.java56
13 files changed, 1049 insertions, 0 deletions
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ClassPathTypeIndex.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ClassPathTypeIndex.java
new file mode 100644
index 000000000..e204d6321
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ClassPathTypeIndex.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+
+/**
+ * Index of java classes to relative absolute paths within repository. Used to generate Git links for binding classes of
+ * VPP apis
+ */
+public class ClassPathTypeIndex implements LinkGenerator {
+
+ private static final String JAVA_SOURCE_FOLDER = "src/main/java";
+ private static final int JAVA_SOURCE_FOLDER_NAME_LENGTH = JAVA_SOURCE_FOLDER.length() + 1;
+
+ /**
+ * <li>key - fully qualified class name value</li><li>value - path within codebase/repository</li>
+ */
+ private final Map<String, String> index;
+
+ public ClassPathTypeIndex(final String projectRoot, final String version) {
+ index = buildIndex(projectRoot, version);
+ }
+
+ /**
+ * <li>input format - LOCAL_ROOT/hc2vpp/module/src/main/java/fully/qualified/class/name/Class.java</li><li>output
+ * format - fully.qualified.class.name.Class</li>
+ */
+ private static String key(String raw) {
+ return raw.substring(raw.indexOf(JAVA_SOURCE_FOLDER) + JAVA_SOURCE_FOLDER_NAME_LENGTH)
+ .replace("/", ".")
+ .replace(".java", "");
+ }
+
+ public String linkForClass(final String clazz) {
+ return index.get(clazz.replace("/", "."));
+ }
+
+ private Map<String, String> buildIndex(final String projectRoot, final String version) {
+ try {
+ return Files.walk(Paths.get(projectRoot))
+ .filter(path -> path.toString().contains("src/main/java"))
+ .filter(path -> path.toString().endsWith(".java"))
+ .map(Path::toString)
+ .map(s -> s.replace(projectRoot, ""))
+ .distinct()
+ .collect(Collectors.toMap(ClassPathTypeIndex::key, o -> generateLink(o, version)));
+ } catch (IOException e) {
+ throw new IllegalStateException(format("%s not found", projectRoot), e);
+ }
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CollectingWriterBuilder.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CollectingWriterBuilder.java
new file mode 100644
index 000000000..b8fcc8b03
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CollectingWriterBuilder.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+
+import com.google.common.collect.ImmutableSet;
+import io.fd.honeycomb.translate.ModifiableSubtreeManagerRegistryBuilder;
+import io.fd.honeycomb.translate.write.Writer;
+import io.fd.honeycomb.translate.write.registry.ModifiableWriterRegistryBuilder;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+
+/**
+ * Implementations of builder that collects handlers as they are bind
+ */
+public class CollectingWriterBuilder implements ModifiableWriterRegistryBuilder {
+
+ private final List<Writer<? extends DataObject>> singleNodeHandlers;
+ private final List<MultiNodeWriteHandler> multiNodeWriteHandlers;
+
+ public CollectingWriterBuilder() {
+ singleNodeHandlers = new LinkedList<>();
+ multiNodeWriteHandlers = new LinkedList<>();
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> add(
+ @Nonnull Writer<? extends DataObject> handler) {
+ singleNodeHandlers.add(handler);
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> subtreeAdd(
+ @Nonnull Set<InstanceIdentifier<?>> handledChildren, @Nonnull Writer<? extends DataObject> handler) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, handledChildren));
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> addBefore(
+ @Nonnull Writer<? extends DataObject> handler, @Nonnull InstanceIdentifier<?> relatedType) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, Collections.singleton(relatedType)));
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> addBefore(
+ @Nonnull Writer<? extends DataObject> handler, @Nonnull Collection<InstanceIdentifier<?>> relatedTypes) {
+ singleNodeHandlers.add(handler);
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> subtreeAddBefore(
+ @Nonnull Set<InstanceIdentifier<?>> handledChildren, @Nonnull Writer<? extends DataObject> handler,
+ @Nonnull InstanceIdentifier<?> relatedType) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, handledChildren));
+ return null;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> subtreeAddBefore(
+ @Nonnull Set<InstanceIdentifier<?>> handledChildren, @Nonnull Writer<? extends DataObject> handler,
+ @Nonnull Collection<InstanceIdentifier<?>> relatedTypes) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, handledChildren));
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> addAfter(
+ @Nonnull Writer<? extends DataObject> handler, @Nonnull InstanceIdentifier<?> relatedType) {
+ singleNodeHandlers.add(handler);
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> addAfter(
+ @Nonnull Writer<? extends DataObject> handler, @Nonnull Collection<InstanceIdentifier<?>> relatedTypes) {
+ singleNodeHandlers.add(handler);
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> subtreeAddAfter(
+ @Nonnull Set<InstanceIdentifier<?>> handledChildren, @Nonnull Writer<? extends DataObject> handler,
+ @Nonnull InstanceIdentifier<?> relatedType) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, Collections.singleton(relatedType)));
+ return this;
+ }
+
+ @Override
+ public ModifiableSubtreeManagerRegistryBuilder<Writer<? extends DataObject>> subtreeAddAfter(
+ @Nonnull Set<InstanceIdentifier<?>> handledChildren, @Nonnull Writer<? extends DataObject> handler,
+ @Nonnull Collection<InstanceIdentifier<?>> relatedTypes) {
+ multiNodeWriteHandlers.add(new MultiNodeWriteHandler(handler, handledChildren));
+ return this;
+ }
+
+ public List<Writer<? extends DataObject>> getSingleNodeHandlers() {
+ return singleNodeHandlers;
+ }
+
+ public List<MultiNodeWriteHandler> getMultiNodeWriteHandlers() {
+ return multiNodeWriteHandlers;
+ }
+
+ public static class MultiNodeWriteHandler {
+ private final Writer<? extends DataObject> writer;
+ private final Set<String> handledChildren;
+
+
+ public MultiNodeWriteHandler(Writer<? extends DataObject> writer, Set<InstanceIdentifier<?>> handledChildren) {
+ this.writer = writer;
+ this.handledChildren = ImmutableSet.<String>builder()
+ .add(writer.getManagedDataObjectType().getTargetType().getName())
+ .addAll(handledChildren.stream()
+ .map(InstanceIdentifier::getTargetType)
+ .map(Class::getName)
+ .collect(Collectors.toSet()))
+ .build();
+ }
+
+ public Writer<? extends DataObject> getWriter() {
+ return writer;
+ }
+
+ public Set<String> getHandledChildren() {
+ return handledChildren;
+ }
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageGenerator.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageGenerator.java
new file mode 100644
index 000000000..4b6ab776c
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageGenerator.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+
+import static io.fd.hc2vpp.docs.api.Operation.CrudOperation.DELETE;
+import static io.fd.hc2vpp.docs.api.Operation.CrudOperation.UPDATE;
+import static io.fd.hc2vpp.docs.api.Operation.CrudOperation.WRITE;
+import static java.lang.String.format;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import io.fd.hc2vpp.docs.api.CoverageUnit;
+import io.fd.hc2vpp.docs.api.JavaApiMessage;
+import io.fd.hc2vpp.docs.api.Operation;
+import io.fd.hc2vpp.docs.api.PluginCoverage;
+import io.fd.hc2vpp.docs.api.YangType;
+import io.fd.honeycomb.translate.write.WriterFactory;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.reflections.ReflectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CoverageGenerator implements VppApiUtils {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CoverageGenerator.class);
+
+ private final CollectingWriterBuilder writerBuilder;
+
+ public CoverageGenerator() {
+ writerBuilder = new CollectingWriterBuilder();
+ }
+
+ public PluginCoverage generateConfigCoverage(final Class<?> pluginClass,
+ final String version,
+ final List<Module> scannedModules,
+ final YangTypeLinkIndex yangTypeIndex,
+ final ClassPathTypeIndex classPathIndex) {
+ LOG.info("Generating config coverage for plugin {}", pluginClass);
+ getInjectedWriterFactories(scannedModules).forEach(writerFactory -> writerFactory.init(writerBuilder));
+
+ LOG.info("Processing single node handlers");
+ final Set<CoverageUnit> singleNodeCoverageUnits = writerBuilder.getSingleNodeHandlers().stream()
+ .flatMap(writer -> {
+ // extracts customizer class from handler
+ final Class<?> customizerClass = getCustomizerClass(writer);
+
+ // scans within write method
+ final Set<PluginMethodReference> writeReferences =
+ new CoverageScanner(customizerClass, WRITE, pluginClass).scan();
+
+ // scans within update method
+ final Set<PluginMethodReference> updateReferences =
+ new CoverageScanner(customizerClass, UPDATE, pluginClass).scan();
+
+ // scans within delete method
+ final Set<PluginMethodReference> deleteReferences =
+ new CoverageScanner(customizerClass, DELETE, pluginClass).scan();
+
+ return Stream.of(writeReferences.stream(), updateReferences.stream(), deleteReferences.stream())
+ .flatMap(pluginMethodReferences -> pluginMethodReferences)
+ .map(reference -> {
+ final CoverageUnit.CoverageUnitBuilder builder = new CoverageUnit.CoverageUnitBuilder();
+
+ // binds vpp api name and generateLink bind with version
+ builder.setVppApi(fromJvppApi(version, reference));
+
+ //binds java api reference
+ builder.setJavaApi(new JavaApiMessage(reference.getName()));
+
+ //binds Yang types with links from pre-build index
+ // TODO - use deserialized yii
+ final String typeName = writer.getManagedDataObjectType().getTargetType().getTypeName();
+ builder.setYangTypes(Collections.singletonList(new YangType(
+ typeName,
+ yangTypeIndex.getLinkForType(typeName))));
+
+ final List<Operation> supportedOperations = new LinkedList<>();
+
+ final String callerClassLink = classPathIndex.linkForClass(reference.getCaller());
+ if (writeReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, WRITE));
+ }
+
+ if (updateReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, UPDATE));
+ }
+
+ if (deleteReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, DELETE));
+ }
+ return builder.setSupportedOperations(supportedOperations).build();
+ });
+ }).collect(Collectors.toSet());
+
+ LOG.info("Processing multi node handlers");
+ final Set<CoverageUnit> multiNodeCoverageUnits = writerBuilder.getMultiNodeWriteHandlers().stream()
+ .flatMap(handler -> {
+ final Class<?> customizerClass = getCustomizerClass(handler.getWriter());
+ final Set<PluginMethodReference> writeReferences =
+ new CoverageScanner(customizerClass, WRITE, pluginClass).scan();
+
+ final Set<PluginMethodReference> updateReferences =
+ new CoverageScanner(customizerClass, UPDATE, pluginClass).scan();
+
+ final Set<PluginMethodReference> deleteReferences =
+ new CoverageScanner(customizerClass, DELETE, pluginClass).scan();
+
+ return Stream.of(writeReferences.stream(), updateReferences.stream(), deleteReferences.stream())
+ .flatMap(pluginMethodReferenceStream -> pluginMethodReferenceStream)
+ .map(reference -> {
+ final CoverageUnit.CoverageUnitBuilder builder = new CoverageUnit.CoverageUnitBuilder();
+ builder.setVppApi(fromJvppApi(version, reference));
+ builder.setJavaApi(new JavaApiMessage(reference.getName()));
+
+ builder.setYangTypes(handler.getHandledChildren().stream()
+ .map(type -> new YangType(type, yangTypeIndex.getLinkForType(type)))
+ .collect(Collectors.toList()));
+
+ final String callerClassLink = classPathIndex.linkForClass(reference.getCaller());
+ final List<Operation> supportedOperations = new LinkedList<>();
+ if (writeReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, WRITE));
+ }
+
+ if (updateReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, UPDATE));
+ }
+
+ if (deleteReferences.contains(reference)) {
+ supportedOperations.add(new Operation(callerClassLink, DELETE));
+ }
+ return builder.setSupportedOperations(supportedOperations).build();
+ });
+ }).collect(Collectors.toSet());
+
+ return new PluginCoverage(pluginClass.getSimpleName(),
+ Stream.of(singleNodeCoverageUnits.stream(), multiNodeCoverageUnits.stream())
+ .flatMap(coverageUnitStream -> coverageUnitStream)
+ .collect(Collectors.toSet()), true);
+ }
+
+ private static Class<?> getCustomizerClass(final Object handler) {
+ try {
+ final Set<Field> customizerFields =
+ ReflectionUtils.getAllFields(handler.getClass(), field -> "customizer".equals(field.getName()));
+ final Field customizerField = customizerFields.iterator().next();
+ customizerField.setAccessible(true);
+ return customizerField.get(handler).getClass();
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(format("Unable to get customizer from %s ", handler), e);
+ }
+ }
+
+ private static Set<WriterFactory> getInjectedWriterFactories(final List<Module> scannedModules) {
+ Injector injector = Guice.createInjector(scannedModules);
+ TypeLiteral<Set<WriterFactory>> writerFactoryType = new TypeLiteral<Set<WriterFactory>>() {
+ };
+ return injector.getInstance(Key.get(writerFactoryType));
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageScanner.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageScanner.java
new file mode 100644
index 000000000..c1c67d77b
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/CoverageScanner.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+
+import static java.lang.String.format;
+
+import io.fd.hc2vpp.docs.api.Operation;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import org.objectweb.asm.ClassReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Scans provided class for reference under specified crud method
+ */
+public class CoverageScanner {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CoverageScanner.class);
+
+ private final Class<?> classToScan;
+ private final Operation.CrudOperation crudOperation;
+ private final Class<?> referenceClass;
+
+ public CoverageScanner(final Class<?> classToScan,
+ final Operation.CrudOperation crudOperation,
+ final Class<?> referenceClass) {
+ this.classToScan = classToScan;
+ this.crudOperation = crudOperation;
+ this.referenceClass = referenceClass;
+ }
+
+ static ClassReader loadClass(String className) {
+ try (InputStream classStream =
+ CoverageScanner.class.getClassLoader().getResourceAsStream(className + ".class")) {
+ return new ClassReader(classStream);
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Unable to load bytecode for class %s", className), e);
+ }
+ }
+
+ public Set<PluginMethodReference> scan() {
+ LOG.debug("Scanning class {}", classToScan.getName());
+ final ClassReader classReader = loadClass(byteCodeStyleReference(classToScan.getName()));
+ final Set<PluginMethodReference> foundReferences = Collections.synchronizedSet(new HashSet<>());
+ classReader.accept(new MethodDelegatingClassVisitor(byteCodeStyleReference(classToScan.getName()),
+ crudOperation.getMethodReference(), byteCodeStyleReference(referenceClass.getPackage().getName()),
+ foundReferences, null), ClassReader.SKIP_DEBUG);
+ return foundReferences;
+ }
+
+ // converts java style reference to bytecode-style name(with slashes instead of dots)
+ private static String byteCodeStyleReference(final String javaStyleName) {
+ return javaStyleName.replace(".", "/");
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/LinkGenerator.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/LinkGenerator.java
new file mode 100644
index 000000000..3226639b1
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/LinkGenerator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+public interface LinkGenerator {
+
+ static String resolveBranch(final String version) {
+ if (version.contains("SNAPSHOT")) {
+ return "master";
+ } else {
+ return "stable%2F" + version.replace(".", "");
+ }
+ }
+
+ default String generateLink(final String raw, final String version) {
+ //https://git.fd.io/hc2vpp/tree/interface-role/api/src/main/yang/interface-role@2017-06-15.yang?h=stable%2F1707
+ return "https://git.fd.io/hc2vpp/tree" + raw + "?h=" + resolveBranch(version);
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodDelegatingClassVisitor.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodDelegatingClassVisitor.java
new file mode 100644
index 000000000..39a06e626
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodDelegatingClassVisitor.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+
+import java.util.Set;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class MethodDelegatingClassVisitor extends ClassVisitor {
+
+ private final String currentClass;
+ private final String methodName;
+ private final String reference;
+ private final Set<PluginMethodReference> foundReferences;
+ private final Set<String> allreadyProcessedLocalMethods;
+
+ public MethodDelegatingClassVisitor(String currentClass,
+ String methodName,
+ String reference,
+ Set<PluginMethodReference> foundReferences,
+ Set<String> allreadyProcessedLocalMethods) {
+ super(Opcodes.ASM5);
+ this.currentClass = currentClass;
+ this.methodName = methodName;
+ this.reference = reference;
+ this.foundReferences = foundReferences;
+ this.allreadyProcessedLocalMethods = allreadyProcessedLocalMethods;
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+ if (name.equals(methodName)) {
+ return new MethodPluginCoverageVisitor(currentClass, foundReferences, reference,
+ allreadyProcessedLocalMethods);
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodPluginCoverageVisitor.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodPluginCoverageVisitor.java
new file mode 100644
index 000000000..fe15f5e79
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/MethodPluginCoverageVisitor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import static java.lang.String.format;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MethodPluginCoverageVisitor extends MethodVisitor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodPluginCoverageVisitor.class);
+
+ private final String currentClass;
+ private final Set<PluginMethodReference> foundReferences;
+ private final String reference;
+ private final Set<String> allreadyProcessedLocal;
+
+ public MethodPluginCoverageVisitor(String currentClass, Set<PluginMethodReference> foundReferences,
+ String reference,
+ Set<String> allreadyProcessedLocal) {
+ super(Opcodes.ASM5);
+ this.currentClass = currentClass;
+ this.foundReferences = foundReferences;
+ this.reference = reference;
+ // if nonnull then reuse
+ this.allreadyProcessedLocal = allreadyProcessedLocal == null
+ ? new HashSet<>()
+ : allreadyProcessedLocal;
+ }
+
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean isInterface) {
+ final String normalizedOwner = owner.replace(";", "").replace("[L", "");
+ if (normalizedOwner.contains(reference)) {
+ // reference was found directly in method code
+ foundReferences.add(new PluginMethodReference(currentClass, owner, name));
+ } else {
+ if (normalizedOwner.contains("io/fd")) {
+ // filter just our method to reduce scope
+ if (!normalizedOwner.equals(currentClass)) {
+ LOG.debug("Processing non-current {}.{}()", normalizedOwner, name);
+ // method call is from different class than currently processed one
+ ClassReader classReader = CoverageScanner.loadClass(normalizedOwner);
+ classReader.accept(new MethodDelegatingClassVisitor(normalizedOwner, name, reference,
+ foundReferences,
+ allreadyProcessedLocal), ClassReader.SKIP_DEBUG);
+ } else {
+ LOG.debug("Processing current {}.{}()", normalizedOwner, name);
+ // other methods in same class that are used in visited method
+ String fullyQualifiedMethodName = fullyQualifiedMethodName(normalizedOwner, name, desc);
+ if (allreadyProcessedLocal.contains(fullyQualifiedMethodName)) {
+ //skip already processed local methods to prevent stack overflow
+ return;
+ }
+ allreadyProcessedLocal.add(fullyQualifiedMethodName);
+
+ ClassReader classReader = CoverageScanner.loadClass(normalizedOwner);
+ classReader.accept(new MethodDelegatingClassVisitor(normalizedOwner, name, reference,
+ foundReferences,
+ allreadyProcessedLocal), ClassReader.SKIP_DEBUG);
+ }
+ }
+ }
+ super.visitMethodInsn(opcode, owner, name, desc, isInterface);
+ }
+
+ private String fullyQualifiedMethodName(String owner, String name, String desc) {
+ return format("%s_%s_%s", owner, name, desc);
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelLinkIndex.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelLinkIndex.java
new file mode 100644
index 000000000..c1b299826
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelLinkIndex.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Index from module file name to git generateLink
+ */
+class ModelLinkIndex implements LinkGenerator {
+
+ private final Map<String, String> modelLinkIndex;
+
+ /**
+ * @param projectRoot for ex.: /home/jsrnicek/Projects/hc2vpp
+ * @param version for ex.: 17.07 to get generateLink for correct branch
+ */
+ ModelLinkIndex(final String projectRoot, final String version) {
+ modelLinkIndex = buildIndex(projectRoot, version);
+ }
+
+ private static String key(String raw) {
+ return raw.substring(raw.lastIndexOf("/"))
+ .replace("/", "")
+ .replace(".yang", "");
+ }
+
+ String linkForModel(final String model, final String revision) {
+ // TODO - figure out how to get revision for model in src,to use YangModelKey
+ // if not local model,add generateLink to ietf datatracker
+ return Optional.ofNullable(modelLinkIndex.get(model + "@" + revision))
+ .orElse(Optional.ofNullable(modelLinkIndex.get(model))
+ .orElse("https://datatracker.ietf.org/"));
+ }
+
+ private Map<String, String> buildIndex(final String projectRoot, final String version) {
+ try {
+ return Files.walk(Paths.get(projectRoot))
+ .filter(path -> path.toString().contains("src/main/yang"))
+ .filter(path -> path.toString().endsWith(".yang"))
+ .map(Path::toString)
+ .map(s -> s.replace(projectRoot, ""))
+ .distinct()
+ .collect(Collectors.toMap(ModelLinkIndex::key, o -> generateLink(o, version)));
+ } catch (IOException e) {
+ throw new IllegalStateException(format("%s not found", projectRoot), e);
+ }
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelTypeIndex.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelTypeIndex.java
new file mode 100644
index 000000000..9fd5976b7
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/ModelTypeIndex.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import static java.util.stream.Collectors.toMap;
+
+import com.google.common.io.Resources;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.opendaylight.yangtools.yang.binding.YangModelBindingProvider;
+import org.opendaylight.yangtools.yang.binding.YangModuleInfo;
+
+/**
+ * Maps namespaces to models
+ */
+class ModelTypeIndex {
+
+ private final Map<YangModelKey, String> namespaceToModuleIndex;
+
+ ModelTypeIndex() throws IOException {
+ namespaceToModuleIndex = collectAllModules(this.getClass().getClassLoader())
+ .stream()
+ .collect(toMap(YangModelKey::new, YangModuleInfo::getName));
+ }
+
+ private static YangModelBindingProvider getModuleBindingProviderInstance(final Class<?> aClass) {
+ try {
+ return (YangModelBindingProvider) aClass.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static Class<?> loadClass(final ClassLoader classLoader, final String name) {
+ try {
+ return classLoader.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+
+ }
+ }
+
+ private static List<String> loadResource(final URL url) {
+ try {
+ return Resources.readLines(url, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ String namespaceToModule(final String namespace,
+ final String revision) {
+ return namespaceToModuleIndex.get(new YangModelKey(namespace, revision));
+ }
+
+ private Set<YangModuleInfo> collectAllModules(final ClassLoader classLoader) throws IOException {
+ return Collections.list(classLoader.getResources("META-INF/services/" +
+ YangModelBindingProvider.class.getName()))
+ .stream()
+ .map(ModelTypeIndex::loadResource)
+ .flatMap(Collection::stream)
+ .map(name -> loadClass(classLoader, name))
+ .map(ModelTypeIndex::getModuleBindingProviderInstance)
+ .map(YangModelBindingProvider::getModuleInfo)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/PluginMethodReference.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/PluginMethodReference.java
new file mode 100644
index 000000000..5a98639ae
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/PluginMethodReference.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+/**
+ * Represent found reference of plugin method
+ */
+public class PluginMethodReference {
+
+ /**
+ * Name of the class that uses such reference
+ */
+ private final String caller;
+
+ /**
+ * Class of the reference
+ */
+ private final String owner;
+
+ /**
+ * Name of the reference
+ */
+ private final String name;
+
+ public PluginMethodReference(final String caller, final String owner, final String name) {
+ this.caller = caller;
+ this.owner = owner;
+ this.name = name;
+ }
+
+ public String getCaller() {
+ return caller;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/VppApiUtils.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/VppApiUtils.java
new file mode 100644
index 000000000..f21cc2660
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/VppApiUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import com.google.common.base.CaseFormat;
+import io.fd.hc2vpp.docs.api.VppApiMessage;
+
+
+public interface VppApiUtils {
+
+ static String vppApiFromJavaApi(final String jvppApi) {
+ return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, jvppApi);
+ }
+
+ static String generateVppApiDocLink(final String version, final String vppApi) {
+ //https://docs.fd.io/vpp/17.07/d9/d1d/structvl__api__create__subif__t.html
+ // links are using double underscore
+ //final String doubleUnderscoreApiName = vppApi.replace("_", "__");
+ //return format("https://docs.fd.io/vpp/%s/d9/d1d/structvl__api__%s__t.html", version, doubleUnderscoreApiName);
+
+ // FIXME - generateLink has dynamic part that can be resolved from api name
+ return "https://docs.fd.io/vpp/17.07/annotated.html";
+ }
+
+ default VppApiMessage fromJvppApi(final String version, final PluginMethodReference jvppApi) {
+ final String vppApi = vppApiFromJavaApi(jvppApi.getName());
+ return new VppApiMessage(vppApi, generateVppApiDocLink(version, vppApi));
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangModelKey.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangModelKey.java
new file mode 100644
index 000000000..2bc5bc5da
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangModelKey.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;import org.opendaylight.yangtools.yang.binding.YangModuleInfo;
+
+final class YangModelKey {
+ private final String namespace;
+ private final String revision;
+
+ YangModelKey(final YangModuleInfo moduleInfo) {
+ this.namespace = moduleInfo.getNamespace();
+ this.revision = moduleInfo.getRevision();
+ }
+
+ YangModelKey(final String namespace, final String revision) {
+ this.namespace = namespace;
+ this.revision = revision;
+ }
+
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public String getRevision() {
+ return revision;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final YangModelKey that = (YangModelKey) o;
+
+ if (namespace != null
+ ? !namespace.equals(that.namespace)
+ : that.namespace != null) {
+ return false;
+ }
+ return revision != null
+ ? revision.equals(that.revision)
+ : that.revision == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = namespace != null
+ ? namespace.hashCode()
+ : 0;
+ result = 31 * result + (revision != null
+ ? revision.hashCode()
+ : 0);
+ return result;
+ }
+}
diff --git a/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangTypeLinkIndex.java b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangTypeLinkIndex.java
new file mode 100644
index 000000000..8220d7f78
--- /dev/null
+++ b/vpp-integration/api-docs/core/src/main/java/io/fd/hc2vpp/docs/core/YangTypeLinkIndex.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2017 Cisco and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.fd.hc2vpp.docs.core;
+
+import static java.lang.String.format;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import org.opendaylight.yangtools.yang.common.QName;
+
+public class YangTypeLinkIndex {
+
+ private final ModelLinkIndex modelLinkIndex;
+ private final ModelTypeIndex modelTypeIndex;
+
+ public YangTypeLinkIndex(final String projectRoot, final String version) {
+ modelLinkIndex = new ModelLinkIndex(projectRoot, version);
+ try {
+ modelTypeIndex = new ModelTypeIndex();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public String getLinkForType(final String classname) {
+ final Class<?> loadedClass;
+ final QName qname;
+ try {
+ loadedClass = this.getClass().getClassLoader().loadClass(classname);
+ final Field qnameField = loadedClass.getField("QNAME");
+ qname = QName.class.cast(qnameField.get(null));
+ } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
+ throw new IllegalStateException(format("Unable to extract QNAME from %s", classname), e);
+ }
+
+
+ final String namespace = qname.getNamespace().toString();
+ final String formattedRevision = qname.getFormattedRevision();
+ final String model = modelTypeIndex.namespaceToModule(namespace, formattedRevision);
+ return modelLinkIndex.linkForModel(model, formattedRevision);
+ }
+}