Migration of ODL based application into lighty.io can be really straightforward. But sometimes you have to do more steps in order to run your application in lighty.io. Especially if your application is using OSGI/Karaf specific code. For example when it’s using BundleContext to get registered OSGI services in runtime (what is bad practice in ODL anyway). This is the case in ONAP SDNC project. In this article we will focus on this kind of problems and how we solved them.

BundleContext usage in SDNC

In SDNC BundleContext is mainly used for two things:

  1. Register implementation of some interface (service) to OSGI registry
  2. Get instance of service from OSGI registry

Firstly we are going to show how we dealt with code which registers service through OSGI and then what we did with the second case – when  BundleContext is used to get services from registry.

What we did with the code registering a service directly

Here is sample code of registering service from SDNC MdsalResourceActivator class:

@Override
public void start(BundleContext ctx) throws Exception {
    ...
    SvcLogicResource impl = new ConfigResource(...);
    LOG.debug("Registering MdsalResource service"
            +impl.getClass().getName());
    registrations.add(ctx.registerService(impl.getClass().getName(),
            impl, null));
    ...
}

In this case it is very simple. MdsalResourceActivator implements BundleActivator interface, which means it is initialized automatically by OSGI and after that, its start method is called. This will not happen in lighty.io, because it is not running in OSGI. What we had to do is just initialize SvcLogicResource instance ourselves and store it for later use.

Here is sample code from our factory implementation:

public class MdsalResourcesFactory {
    ...
    public static List<SvcLogicResource> createMdsalResources(...) {
        ...
        List<SvcLogicResource> logicResources = new ArrayList<>(2);
        SvcLogicResource logicResource = new ConfigResource(...);
        LOG.debug(...);
        logicResources.add(logicResource);

        logicResource = new OperationalResource(...);
        LOG.debug(...);
        logicResources.add(logicResource);
        return logicResources;
    }
}

and here is sample code of its usage:

public class SliAdaptorsModule extends AbstractLightyModule {
    ...
    private List<SvcLogicResource> mdsalResources;
    private List<SvcLogicResource> logicResources;
    ...
    @Override
    protected boolean initProcedure() {
        ...
        this.mdsalResources =
                MdsalResourcesFactory.createMdsalResources(...);
        if (this.mdsalResources.isEmpty()) {
            LOG.error("MDSAL logic resources are not present.");
            return false;
        }
        this.logicResources = new ArrayList<>();
        ...
        this.logicResources.addAll(this.mdsalResources);
        return true;
    }
    ...
    public synchronized Optional<List<SvcLogicResource>>
            getSvcLogicResources() {
        if (this.logicResources == null) {
            return Optional.empty();
        }
        return Optional.of(
                Collections.unmodifiableList(this.logicResources));
    }
}

Please note that SliAdaptorsModule class extends AbstractLightyModule class. That means it is a LightyModule which you can start and stop. It’s one of many LightyModules created during SDNC migration.

Also note the getSvcLogicResources() method. Through this method we can access list of all initialized SvcLogicResource implementations (not only ConfigResource and OperationalResource, but also AAIService and SqlResource).

What we did with the code getting a service directly

Now let’s take a look on what we did with second case of BundleContext usage in SDNC. In order to be consistent with previous example, here is code sample from SDNC SvcLogicNodeExecutor class, which uses BundleContext to get SvcLogicResource service (implementation):

 


protected SvcLogicResource getSvcLogicResource(String plugin) {
    BundleContext bctx = FrameworkUtil.getBundle(this.getClass())
            .getBundleContext();
    ServiceReference sref = bctx.getServiceReference(plugin);
    if (sref != null) {
        SvcLogicResource resourcePlugin = (SvcLogicResource) bctx
                .getService(sref);
        return resourcePlugin;
    }
    ...
}

Probably the best way how to remove this code, is to inject required service through constructor or setter. In our case we have copied the whole SvcLogicNodeExecutor class and changed it in a way that we can inject list of SvcLogicResource through the setter method. Here is a sample code of our implementation:


public abstract class SvcLogicNodeExecutor {
    private final Map<String, SvcLogicResource> logicResources;
    ...
    public SvcLogicNodeExecutor() {
        this.logicResources = new HashMap<>();
        ...
    }
    ...
    protected SvcLogicResource getSvcLogicResource(String plugin) {
        return this.logicResources.get(plugin);
    }
    void setLogicResources(Map<String,
            SvcLogicResource> logicResources) {
        this.logicResources.putAll(logicResources);
    }
    ...
}

We changed implementation of the getSvcLogicResource method, so it now does not do any lookup in service registry through BundleContext.

One thing to note is that in previous example we stored SvcLogicResources as list, but here it’s a map. The reason being the keys of this map (thus plugin parameter in getSvcLogicResource method) are just class names of its values (getClass().getName()).

Here is sample code of how we set SvcLogicResources to our SvcLogicNodeExecutor:

private static Map<String, SvcLogicNodeExecutor>
        createBuiltinExecutors(..., Map<String, SvcLogicResource>
                logicResources) {
    Map<String, SvcLogicNodeExecutor> builtinExecutors =
        new HashMap<>();
    builtinExecutors.put("block", new BlockNodeExecutor());
    ...
    builtinExecutors.put("break", new BreakNodeExecutor());
    builtinExecutors.values().forEach(executor -> {
        ...
        executor.setLogicResources(logicResources);
    });
    return builtinExecutors;
}

In this case, we used injection through the setter method, which may not be the best solution in many cases. We will explain why we didn’t use injection through constructor later.

The “Cascade copy” problem

In ideal world, where dependency injection is used properly and code is decomposed in a right way (of course it’s not always possible, that’s why it is ideal), migrating to lighty.io is just a manner of instantiation of classes (beans if you want) which are originally instantiated by some dependency injection system (Blueprint for example). But we don’t live in an ideal world, so things are often more complicated (it would not be fun either way).

One example of such complication is something what we call “Cascade copy” problem. Theory is very simple. Let’s say we have class A which is initialized by your dependency injection system.The class A depends on (uses/extend/whatever) class B and it depends on class C. Now in class C you have some code, which in runtime tries to get some service (for example via BundleContext, to be consistent). Problem is, that if you copy class C and change it’s implementation to get rid of BundleContext you will realize, that you have to also copy class B, because you changed package of class C. And then you have to copy class A even if there will not be other changes, just changes in imports. Some sort of this is also present in SDNC.

In previous example, we showed you snippet from SvcLogicNodeExecutor class. We had to change its original implementation in order to get rid of BundleContext. However, this class is abstract and has 19 implementations. Therefore, we had to copy all these 19 classes, and change import of SvcLogicNodeExecutor to point to our implementation. Now you see why we in SvcLogicNodeExecutor used injection through setter method. It’s simply because we want to avoid too much changes in its 19 implementations.

SvcLogicNodeExecutor implementation was created and registered in SvcLogicActivator class which implements BundleActivator, so we had to rewrite initialization. Here is relevant snippet from SDNC SvcLogicActivator class:

public class SvcLogicActivator implements BundleActivator {
    ...
    private static final Map<String, SvcLogicNodeExecutor>
            BUILTIN_NODES = new HashMap<String,
            SvcLogicNodeExecutor>() {
        {
            put("block", new BlockNodeExecutor());
            put("call", new CallNodeExecutor());
            ... other executors here ...
            put("update", new UpdateNodeExecutor());
            put("break", new BreakNodeExecutor());

        }
    };
    ...
    @Override
    public void start(BundleContext ctx) throws Exception {
        ...
        try {
            SvcLogicStore store = getStore();
            registerNodeTypes(store);
        } catch (ConfigurationException e) {
            LOG.warn("Could not initialize SvcLogicScore", e);
        }
        ...
    }
    ...
    private static void registerNodeTypes(SvcLogicStore store) throws
            SvcLogicException {
        ...
        Hashtable propTable = new Hashtable();
        for (String nodeType : BUILTIN_NODES.keySet()) {
            LOG.info("SLI - registering node type {}", nodeType);
            propTable.clear();
            propTable.put("nodeType", nodeType);
            ServiceRegistration reg = bundleCtx.registerService(
                    SvcLogicNodeExecutor.class.getName(),
                    BUILTIN_NODES.get(nodeType), propTable);
            ...
        }
    }
}



Example of our implementation of executors initialization is here:
public class SvcLogicServiceImpl implements SvcLogicService {
    private final Map<String, SvcLogicNodeExecutor> nodeExecutors;

    public SvcLogicServiceImpl(...) throws SvcLogicException {
        ...
        this.nodeExecutors = createBuiltinExecutors(storeFactory, logicJavaPlugins,
            logicRecorders, logicResources);
    }
    ...
    private static Map<String, SvcLogicNodeExecutor>
            createBuiltinExecutors(... ) {
        Map<String, SvcLogicNodeExecutor> builtinExecutors =
            new HashMap<>();
        builtinExecutors.put("block", new BlockNodeExecutor());
        builtinExecutors.put("call", new CallNodeExecutor());
        ... other executors here ...
        builtinExecutors.put("update", new UpdateNodeExecutor());
        builtinExecutors.put("break", new BreakNodeExecutor());
        ...
    }
    ...
}

The problem summary is simple; we had to just copy all dependent classes and change their imports. In practice it required a little bit more investigation, because we firstly had to recursively find all usages of class to the top of “dependency tree”, where top is some class which is not used directly anywhere (is hidden behind implemented interface, or it’s initialized by dependency injection system and is not injected anywhere). This should be easy, if you know your codebase you are trying to migrate.

Summary

In the article we talked how we dealt with some problems we encountered in migration of ONAP SDNC to lighty.io. There are many more problems in SDNC we didn’t mention. For example, we found:

  • 4 different dependency injection systems used through the project – Blueprint, old ODL ConfigSubsystem, SpringDM, direct use of OSGI service registry
  • Property files are loaded from file system in many places instead of configuration injection.

We also didn’t show you our main class, which integrates everything together (but you may see it in our video).

Categories: lighty.io