Search code examples
javaosgiapache-felix

Can't add the plus button in OSGi Apache Felix Web Console to create new configuration instances


I use a ManagedServiceFactory to create new instances by the parameters that are included in the configuration files. I want to do it by the Apache Felix Web Console but it doesn't give me the plus button to add new configurations.
I think I am missing something. Could you please help me?
This is an image of my Apache Felix Web Console

This is the class that implements ManagedServiceFactory

@org.osgi.service.component.annotations.Component(
        name = "camel_config",
        property = {
                "service.pid=camel",
                "factory=true"
        },
        configurationPolicy = ConfigurationPolicy.IGNORE
)
public class ConfigReaderFactory implements ManagedServiceFactory {
    private static final String DELETE_CONDITIONS = "readLock=changed&idempotent=false&noop=true&delete=true";
    private static final String NON_DELETE_CONDITIONS = "noop=true";
    private volatile DependencyManager dependencyManager;
    private final Map<String, Component> components = new HashMap<>();
    private List<String> attributes;
    private List<String> csvTypes;
    private CamelService camel;
    private TypeConverter converter;
    private EventPublisher publisher;
    private String url;
    private String name;
    private int confLine;
    private String endpointType;
    private String ip;
    private String username;
    private String password;
    private String folder;
    private boolean delete;

    private Double latitude;
    private Double longitude;
    private String email;

    @Override
    public String getName() {
        return this.getClass().getName();
    }

    @Override
    public void updated(String pid, @SuppressWarnings("rawtypes") Dictionary props) throws ConfigurationException {
        if (components.containsKey(pid)) {
            return;
        }
        if (props != null) {
            attributes = new ArrayList<>();
            csvTypes = new ArrayList<>();

            int count = 1;
            String configurationLine;
            while ((configurationLine = (String) props.get(Integer.toString(count++))) != null) {
                List<String> values = Utils.getValuesFromLine(configurationLine);
                attributes.add(values.size() >= 1 ? values.get(0) : TypeConverter.NAMELESS);
                csvTypes.add(values.size() >= 2 ? values.get(1) : TypeConverter.NAMELESS);
            }
            confLine = Integer.parseInt((String) props.get(Config.CONFIG_LINE));
            name = (String) props.get(Config.NAME);

            initConfigParameters(pid, props);
            buildURL();
            System.out.println("[URL] " + url);

            try {
                Map<String, Object> params = new HashMap<>();
                putParameters(params);
                camel.start(params);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void deleted(String pid) {
        Component component = components.remove(pid);
        dependencyManager.remove(component);
        component.stop();
    }

    private void buildURL() {
        url = "";
        switch(endpointType) {
        case Constants.FTP:
            url += "ftp://" + username + "@" + ip + "/" + folder + "?";
            if(!password.equals("")) {
                url += "password=" + password + "&";
            }
            break;
        case Constants.FILE:
            url += "file://" + folder + "?";
            break;
        case Constants.EMAIL:
            url += "imaps://imap.gmail.com?username="+email+"&password="+password
                    +"&delete=false&unseen=false";
        }
        if(endpointType.equals(Constants.FTP) || endpointType.equals(Constants.FILE)) {
            if (delete) {
                url += DELETE_CONDITIONS;
            } else {
                url += NON_DELETE_CONDITIONS;
            }
        }
    }

    private void initConfigParameters(String pid, @SuppressWarnings("rawtypes") Dictionary props) {
        confLine = Integer.parseInt((String) props.get(Config.CONFIG_LINE));
        name = (String) props.get(Config.NAME);
        endpointType = (String) props.get(Config.ENDPOINT_TYPE);
        ip = (String) props.get(Config.IP_ADDRESS);
        username = (String) props.get(Config.USERNAME);
        password = (String) props.get(Config.PASSWORD);
        folder = (String) props.get(Config.FOLDER);
        email = (String) props.get(Config.EMAIL);
        delete = ((String) props.get(Config.DELETE)).equals("true");
        if((String) props.get(Config.LATITUDE)!=null) {
            latitude = Double.parseDouble((String) props.get(Config.LATITUDE));
        } 
        if((String) props.get(Config.LONGITUDE)!=null) {
            longitude = Double.parseDouble((String) props.get(Config.LONGITUDE));
        }

        printParameters(pid);
    }
    private void putParameters(Map<String, Object> params) {
        params.put(Constants.ATTRIBUTES, attributes);
        params.put(Constants.TYPES, csvTypes);
        params.put(Constants.CONF_LINE, confLine);
        params.put(Constants.SOURCE, name);
        params.put(Constants.PUBLISHER, publisher);
        params.put(Constants.CONVERTER, converter);
        params.put(Constants.LATITUDE, latitude);
        params.put(Constants.LONGITUDE, longitude);
        params.put(Constants.ENDPOINT_TYPE, endpointType);
        params.put(Constants.EMAIL, email);
        params.put(Constants.CONTEXT, FrameworkUtil.getBundle(this.getClass()).getBundleContext());
        Processor processor = new CSVProcessor(params);
        params.put(Constants.PROCESSOR, processor);
        params.put(Constants.URL, url);
    }
    private void printParameters(String pid) {
        System.out.println("\nStarting camel with parameters:");
        System.out.println("Config file name "+pid);
        System.out.println("[sensor_name]::" + name);
        if(latitude!=null && longitude!=null) {
            System.out.println("[latitude]::" + latitude);
            System.out.println("[longitude]::" + longitude);
        }
        System.out.println("[endpoint_type]::" + endpointType);
        if(endpointType.equals("ftp")) {
            System.out.println("[ip_address]::" + ip);
            System.out.println("[folder]::" + folder);
            System.out.println("[user]::" + username);
            System.out.println("[password]::" + password);
        } else if(endpointType.equals("file")) {
            System.out.println("[folder]::" + folder);
        } else if(endpointType.equals("email")) {
            System.out.println("[email]::" + email);
            System.out.println("[password]::" + password);
        }
        System.out.println("[delete]::" + delete);
    }

    @Reference(service = TypeConverter.class)
    public void setTypeConverter(TypeConverter converter) {
        this.converter = converter;
    }

    public void unsetTypeConverter(TypeConverter converter) {
        this.converter = null;
    }

    @Reference(service = EventPublisher.class)
    public void setEventPublisher(EventPublisher publisher) {
        this.publisher = publisher;
    }

    public void unsetEventPublisher(EventPublisher publisher) {
        this.publisher = null;
    }

    @Reference(service = CamelService.class)
    public void setCamelService(CamelService camel) {
        this.camel = camel;
    }

    public void unsetCamelService(CamelService camel) {
        this.camel = null;
    }
}

Solution

  • I use a ManagedServiceFactory to create new instances by the parameters that are included in the configuration files.

    You are using Declarative Services (your class is annotated @Component) but also implementing ManagedServiceFactory. As others have mentioned this is a bad practice and you should not do it. Furthermore your code is not thread safe, and the Map<String, Component> components could become corrupted.

    A far better solution is to follow the recommendation of making the Component configurationPolicy=ConfigurationPolicy.REQUIRE and using an @Activate method to receive configuration and an @Destroy method to handle deletions. You then don't need the maps at all.

    I want to do it by the Apache Felix Web Console but it doesn't give me the plus button to add new configurations.

    The Felix Web console is using Metatype to generate this user interface. Writing Metatype XML is horrible, and should not be attempted by humans, but fortunately you can use the org.osgi.service.metatype.annotations to define your metatype. These annotations are applied to an interface or an annotation which describes the layout of your configuration. For example:

    @ObjectClassDefinition
    public @interface MyConfig {
    
        // This defines the property key "myProp" with
        // a default value of "foo" and it will show
        // up in the UI with the name "My Prop"
        String myProp() default "foo";
    
        // This defines the property key "my.prop"
        // with a default value of 42. The @AD
        // allows customisation of the UI
        @AD(description="Something descriptive")
        int my_num() default 42;
    }
    

    You link this configuration property type with your component using the @Designate annotation:

    @org.osgi.service.component.annotations.Component(
        name = "camel_config", configurationPid=camel",
        configurationPolicy = ConfigurationPolicy.REQUIRE
    )
    @Designate(ocd=MyConfig.class)
    public class ConfigReaderFactory {
        ...
    }
    

    When using DS 1.3 you can also inject this configuration type directly into your activate method.

    @Activate
    void activate(MyConfig config) {
        // process config...
    }
    

    You can also inject the Map as well if you like:

    @Activate
    void activate(MyConfig config, Map<String, Object> rawConfig) {
        // process config...
    }
    

    Once your metatype is defined the + icon will appear, along with a custom UI for configuring your component.