2016/05/09
9 May, 2016

[Tutorial] How To Extend Default Subscription Model in WSO2 App Manager - Part 2

  • Sajith Abeywardhana
  • Software Engineer - WSO2
Archived Content
This article is provided for historical perspective only, and may not reflect current conditions. Please refer to relevant product page for more up-to-date product information and resources.

Table of contents


Introduction

This tutorial is the second part of the How To Extend Default Subscription Model in WSO2 App Manager tutorial series. If you haven’t already done so, we advise you to read through and understand the first tutorial before referring to this one.

In this tutorial we are going to implement an API that allows you to retrieve user subscription details by using the hostobject model. This way, anyone would call that API to do that function. Since this is a custom extension we will extend WSO2 App Manager’s code by adding new code as a separate file without editing the default file.


WSO2 App Manager

WSO2 App Manager is a solution that provides application governance in an enterprise. It can be used to manage and access day-to-day Web and mobile applications in a single location providing centralized access to multiple apps. Another advantage is that when publishers and developers publish unsecured apps, WSO2 App Manager will take care of aspects such as security, throttling and statistics among other things. The end user can see the published apps and subscribe to them. Businesses can leverage its single-sign-on (SSO) functionality, which can reduce help desk and administrative costs. It also eliminates the requirement to memorize a long list of passwords. WSO2 App Manager comprises of the following key components:

  • App Publisher - Enables the end-user to easily publish their apps, share documentation, and gather feedback on the quality and usage of apps.
  • App Store - Enables the end-user to easily access Web applications and mobile application to self-register, discover apps, subscribe to apps, and evaluate them.

Develop the model class

  1. Create a new package named org.wso2.carbon.appmgt.expiring.subscription.api.model.
  2. Now create a new Java class called SubscribedAppExtension.
  3. Your data transfer object (DTO) class should be similar to what’s shown below. You can copy-paste the below class definition:
    package org.wso2.carbon.appmgt.expiring.subscription.api.model;
    
    import org.wso2.carbon.appmgt.api.model.APIIdentifier;
    import org.wso2.carbon.appmgt.api.model.SubscribedAPI;
    
    import java.util.Date;
    
    public class SubscribedAppExtension {
    
       private Date subscriptionTime;
       // Evaluation period must be provided as hours.
       private int evaluationPeriod;
    
       private int subscriptionID;
    
       private Date expireOn;
    
       private boolean isPaid;
    
       private SubscribedAPI subscribedApp;
    
       public SubscribedAppExtension(APIIdentifier apiIdentifier) {
           subscribedApp = new SubscribedAPI(null, apiIdentifier);
       }
    
       public boolean isPaid() {
           return isPaid;
       }
    
       public void setPaid(boolean isPaid) {
           this.isPaid = isPaid;
       }
    
       public int getSubscriptionID() {
           return subscriptionID;
       }
    
       public void setSubscriptionID(int subscriptionID) {
           this.subscriptionID = subscriptionID;
       }
    
       public Date getSubscriptionTime() {
           return subscriptionTime;
       }
    
       public void setSubscriptionTime(Date subscriptionTime) {
           this.subscriptionTime = subscriptionTime;
       }
    
       public int getEvaluationPeriod() {
           return evaluationPeriod;
       }
    
       public void setEvaluationPeriod(int evaluationPeriod) {
           this.evaluationPeriod = evaluationPeriod;
       }
    
       public Date getExpireOn() {
           return expireOn;
       }
    
       public void setExpireOn(Date expireOn) {
           this.expireOn = expireOn;
       }
    
       public SubscribedAPI getSubscribedApp() {
           return subscribedApp;
       }
    
       public void setSubscribedApp(SubscribedAPI subscribedApp) {
           this.subscribedApp = subscribedApp;
       }
    }
    

Implement the method in the data access object (DAO) class

Open the AppMSubscriptionExtensionDAO class and add the below method to retrieve the user subscription details.

/**
* This method returns the set of subscriptions for the given user.
*
* @param String user
* @return List
* @throws org.wso2.carbon.appmgt.api.AppManagementException if failed to get SubscribedAPIs
*/
public List getSubscribedApps(String user)
       throws
       AppManagementException {
   ArrayList subscribedApps = new ArrayList();
   Connection connection = null;
   PreparedStatement ps = null;
   ResultSet result = null;

   try {
       connection = APIMgtDBUtil.getConnection();

       String sqlQuery =
               "SELECT " + "   SUBS.SUBSCRIPTION_ID"
                       + "   ,API.APP_PROVIDER AS APP_PROVIDER"
                       + "   ,API.APP_NAME AS APP_NAME"
                       + "   ,API.APP_VERSION AS APP_VERSION"
                       + "   ,API.APP_ID AS APP_ID"
                       + "   ,SUBS.SUBSCRIPTION_TIME AS SUBSCRIPTION_TIME"
                       + "   ,SUBS.EVALUATION_PERIOD AS EVALUATION_PERIOD"
                       + "   ,SUBS.EXPIRED_ON AS EXPIRED_ON"
                       + "   ,SUBS.IS_PAID AS IS_PAID"
                       + "   FROM "
                       + "   APM_SUBSCRIBER SUB, APM_SUBSCRIPTION_EXT SUBS, APM_APP API "
                       + "   WHERE " + "   SUB.USER_ID = ? "
                       + "   AND SUB.TENANT_ID = ? "
                       + "   AND SUB.SUBSCRIBER_ID=SUBS.SUBSCRIBER_ID "
                       + "   AND API.APP_ID=SUBS.APP_ID";

       ps = connection.prepareStatement(sqlQuery);
       ps.setString(1, user);
       int tenantId = IdentityUtil.getTenantIdOFUser(user);
       ps.setInt(2, tenantId);
       result = ps.executeQuery();

       if (result == null) {
           return subscribedApps;
       }

       while (result.next()) {
           APIIdentifier apiIdentifier = new APIIdentifier(
                   AppManagerUtil.replaceEmailDomain(result.getString("APP_PROVIDER")),
                   result.getString("APP_NAME"),
                   result.getString("APP_VERSION")
           );
           apiIdentifier.setApplicationId(result.getString("APP_ID"));
           SubscribedAppExtension subscribedAPI = new SubscribedAppExtension(apiIdentifier);
           subscribedAPI.setSubscriptionID(result.getInt("SUBSCRIPTION_ID"));
           subscribedAPI.setSubscriptionTime(result.getTimestamp("SUBSCRIPTION_TIME"));
           subscribedAPI.setEvaluationPeriod(result.getInt("EVALUATION_PERIOD"));
           subscribedAPI.setExpireOn(result.getTimestamp("EXPIRED_ON"));
           subscribedAPI.setPaid(result.getBoolean("IS_PAID"));
           subscribedApps.add(subscribedAPI);
       }
   } catch (SQLException e) {
       handleException("Failed to get SubscribedAPI of : " + user, e);
   } catch (IdentityException e) {
       handleException("Failed get tenant id of user " + user, e);
   } finally {
       APIMgtDBUtil.closeAllConnections(ps, connection, result);
   }
   return subscribedApps;
}

private static void handleException(String msg, Throwable t) throws AppManagementException {
   log.error(msg, t);
   throw new AppManagementException(msg, t);
}

The implementation of this method will query the database subscription table and constructs the user subscriptionApp list. Then the subscriptionApp list will be returned to the caller method. It contains user-wise app subscription details such as application name, version, provider, subscription ID, subscribed time, evaluation period, expiry date/time and paid/unpaid status.


Define the interface

Now create a new Java interface called AppConsumerExtension in the org.wso2.carbon.appmgt.expiring.subscription.api package. Define a method to retrieve the user subscription as shown below:

package org.wso2.carbon.appmgt.expiring.subscription.api;

import org.wso2.carbon.appmgt.api.AppManagementException;
import org.wso2.carbon.appmgt.expiring.subscription.api.model.SubscribedAppExtension;

import java.util.List;

public interface AppConsumerExtension {

   public List getSubscribedApps(String user) throws
                                                                      AppManagementException;
}

Implement the interface

Now we have to implement the above interface. For that,

  1. Create a new Java class called AppConsumerExtensionImpl in the org.wso2.carbon.appmgt.expiring.subscription.impl package by implementing the above interface.
  2. Implement the getSubscribedApps method as shown below.
    package org.wso2.carbon.appmgt.expiring.subscription.impl;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.wso2.carbon.appmgt.api.AppManagementException;
    import org.wso2.carbon.appmgt.expiring.subscription.api.AppConsumerExtension;
    import org.wso2.carbon.appmgt.expiring.subscription.api.model.SubscribedAppExtension;
    import org.wso2.carbon.appmgt.expiring.subscription.impl.dao.AppMSubscriptionExtensionDAO;
    
    import java.util.List;
    
    public class AppConsumerExtensionImpl implements AppConsumerExtension {
    
       private static final Log log = LogFactory.getLog(AppConsumerExtensionImpl.class);
    
       public List getSubscribedApps(String user) throws
                                                                          AppManagementException {
           AppMSubscriptionExtensionDAO appMSubscriptionExtensionDAO = new AppMSubscriptionExtensionDAO();
           List subscribedAppsList = appMSubscriptionExtensionDAO.getSubscribedApps(user);
           return subscribedAppsList;
       }
    }
    

Here, we call the AppMSubscriptionExtensionDAO class and getSubscribedApps method by passing the user as a parameter. When we do that we can get the subscriptionApps list according to the given user.

Now we need to implement the API interface coding. For that purpose we use the Store Jaggery application which is shipped with default WSO2 App Manager.


Create the new hostobject and API interface

We can integrate the Java object (the implementation of the API) and the Jaggery app by using the hostobject mechanism. So we use the hostobject model and create a new hostobject name called AppStoreExtensionHostObject. Then we define the fully qualified host object call name in the module.xml file

  1. Create the hostObject

    Now create a new package called org.wso2.carbon.appmgt.expiring.subscription.hostobjects. Then define a new Java class called AppStoreExtensionHostObject in that package as shown below.

    /*
    *  Copyright (c) 2016, WSO2 Inc. (https://www.wso2.org) All Rights Reserved.
    *
    *  WSO2 Inc. licenses this file to you 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
    *
    *    https://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 org.wso2.carbon.appmgt.expiring.subscription.hostobjects;
    
    import org.jaggeryjs.scriptengine.exceptions.ScriptException;
    import org.mozilla.javascript.Context;
    import org.mozilla.javascript.Function;
    import org.mozilla.javascript.NativeArray;
    import org.mozilla.javascript.NativeObject;
    import org.mozilla.javascript.Scriptable;
    import org.mozilla.javascript.ScriptableObject;
    import org.wso2.carbon.appmgt.api.AppManagementException;
    import org.wso2.carbon.appmgt.expiring.subscription.api.AppConsumerExtension;
    import org.wso2.carbon.appmgt.expiring.subscription.api.model.SubscribedAppExtension;
    import org.wso2.carbon.appmgt.expiring.subscription.impl.AppConsumerExtensionImpl;
    import org.wso2.carbon.appmgt.impl.utils.AppManagerUtil;
    import org.wso2.carbon.base.MultitenantConstants;
    import org.wso2.carbon.context.PrivilegedCarbonContext;
    import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
    
    import java.text.DateFormat;
    import java.text.SimpleDateFormat;
    import java.util.List;
    
    public class AppStoreExtensionHostObject extends ScriptableObject {
    
       private AppConsumerExtension appConsumerExtension;
    
       private static final String hostObjectName = "AppStoreExtension";
    
       public static Scriptable jsConstructor(Context cx, Object[] args, Function Obj,
                                              boolean inNewExpr)
               throws ScriptException, AppManagementException {
           return new AppStoreExtensionHostObject();
       }
    
       public AppStoreExtensionHostObject() throws AppManagementException {
           appConsumerExtension = new AppConsumerExtensionImpl();
       }
    
       @Override
       public String getClassName() {
           return hostObjectName;
       }
    
       public AppConsumerExtension getAppConsumerExtension() {
           return appConsumerExtension;
       }
    
       public void setAppConsumerExtension(AppConsumerExtension appConsumerExtension) {
           this.appConsumerExtension = appConsumerExtension;
       }
    
       private static AppConsumerExtension getAppConsumer(Scriptable thisObj) {
           return ((AppStoreExtensionHostObject) thisObj).getAppConsumerExtension();
       }
    
       public static NativeArray jsFunction_getSubscriptionsByUser(Context cx,
                                                                   Scriptable thisObj, Object[] args, Function funObj)
               throws ScriptException, AppManagementException {
           DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
           NativeArray myn = new NativeArray(0);
           if (args != null && isStringArray(args)) {
               String userName = (String) args[0];
               boolean isTenantFlowStarted = false;
               try {
                   String tenantDomain = MultitenantUtils.getTenantDomain(AppManagerUtil.replaceEmailDomainBack(userName));
                   if (tenantDomain != null && !MultitenantConstants.SUPER_TENANT_DOMAIN_NAME.equals(tenantDomain)) {
                       isTenantFlowStarted = true;
                       PrivilegedCarbonContext.startTenantFlow();
                       PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tenantDomain, true);
                   }
                   AppConsumerExtension appConsumer = getAppConsumer(thisObj);
                   List subscribedAppsList = appConsumer.getSubscribedApps(userName);
    
                   int i = 0;
                   for (SubscribedAppExtension subscribedApp : subscribedAppsList) {
                       NativeObject row = new NativeObject();
                       row.put("subscriptionId", row, subscribedApp.getSubscriptionID());
                       row.put("appId", row, subscribedApp.getSubscribedApp().getApiId().getApplicationId());
                       row.put("appName", row, subscribedApp.getSubscribedApp().getApiId().getApiName());
                       row.put("appVersion", row, subscribedApp.getSubscribedApp().getApiId().getVersion());
                       row.put("appProvider", row, AppManagerUtil.replaceEmailDomainBack(
                               subscribedApp.getSubscribedApp().getApiId().getProviderName()));
                       row.put("subscriptionTime", row, dateFormat.format(subscribedApp.getSubscriptionTime()));
                       row.put("evaluationPeriod", row, subscribedApp.getEvaluationPeriod());
                       row.put("expiredOn", row, dateFormat.format(subscribedApp.getExpireOn()));
                       row.put("isPaid", row, subscribedApp.isPaid());
                       myn.put(i, myn, row);
                       i++;
                   }
               } finally {
                   if (isTenantFlowStarted) {
                       PrivilegedCarbonContext.endTenantFlow();
                   }
               }
           }
           return myn;
       }
    
       public static boolean isStringArray(Object[] args) {
           int argsCount = args.length;
           for (int i = 0; i < argsCount; i++) {
               if (!(args[i] instanceof String)) {
                   return false;
               }
           }
           return true;
       }
    }
    

    Now we are done with the Java coding. We can build the Java project as discussed in part one of this tutorial (refer to the “Build the project and update the configurations to execute the custom extension” section).

  2. Create the new module

    WSO2 App Manager has many modules that can be used to integrate different subsystems with itself. For example, all webapp asset type APIs and a many number of mobileapp asset type APIs are use this mechanism. We will use the same model so that the API integrations are more consistent.

    • Create a new folder called appstoreextension in the <AppM-Home>/modules/ directory and create a new file call module.xml in the in the appstoreextension folder.
    • Now we have a new module called appstoreextension. In this module we should define a hostobject as shown below. We use module.xml for the definition. Add the below code segment to the module.xml file:
      <module name="appstoreextension" namespace="ns" expose="true" xmlns="https://wso2.org/projects/jaggery/module.xml">
          <hostObject>
              <className>org.wso2.carbon.appmgt.expiring.subscription.hostobjects.AppStoreExtensionHostObject</className>
              <name>AppStoreExtension</name>
          </hostObject>
      </module>
      
    • <module name="appstoreextension"> property should be same as the directory name contained in the module.xml.
    • <className> should be the fully qualified class name of the hostobject class.
    • <name> element’s value should be equal to the hostobject getClassName() method’s return value.
  3. Create the hostobject, object using Jaggery.

    We use Jaggery code to create a new hostobject. Without adding new code segment to the existing manager model we use a new extension module here. This is a best practice because the objective is to extend the default behavior of WSO2 App Manager. So we need to make sure that the default app manager implementations is kept as it is and new files are used to extend the custom requirement.

    • Create a new file called manager_extension.jag in the <APPM-HOME>/resources/repository/deployment/server/jaggeryapps/store/modules/manager/ directory.
    • Copy and paste the below Jaggery code segment in the manager_extension.jag file.
      <%
      var getAppStoreExtensionObj = function () {
      
          var tenantDomain = request.getParameter("tenant");
          var user = jagg.getUser();
          var store = require('appstoreextension');
          if (user == null) {
              var storeExtensionHostObj = new store.AppStoreExtension();
              if (tenantDomain != null && tenantDomain != "") {
                  storeExtensionHostObj.loadRegistryOfTenant(tenantDomain);
              }
              return storeExtensionHostObj;
          } else {
              return new store.AppStoreExtension();
          }
      };
      %>
      
    • Here, we get a reference to the module created in step 1. Then we create a new hostobject by using that reference and return it.
  4. Extend the manager module.

    Now we need to introduce a new function in the manager module to obtain the new object of the hostobject. In order to do so you need to edit the module.jag file in the <APPM-HOME>/resources/repository/deployment/server/jaggeryapps/store/modules/manager/ directory.

    • Open the module.jag file using a text editor and add the code segment below. You can add this code segment just below the getAPIStoreObj function definition.
      getAppStoreExtensionObj: function () {
          return jagg.require(jagg.getModulesDir() + "manager/manager_extension.jag").getAppStoreExtensionObj.apply(this, arguments);
      },
      
  5. Add new URL mapping in jaggery.conf to locate the API.

    All the APIs are mapped in the jaggery.conf file. Each Jaggery app and the store app has this file. We need to map the API resource path according to the API URL mapping.

  6. Open the store’s jaggery.conf file using a text editor and add the code segment below. You can add this code segment just below the subscriptions API URL mapping.
    {
        "url": "/subscriptionsExtension/*",
        "path": "/apis/subscriptions_extension.jag"
    },
    

    According to this URL mapping all API calls that contain the /store/subscriptionsExtension/ URL will route to the /apis/subscriptions_extension.jag file.

  7. Now we have to write the API definition In the subscriptions_extension.jag file. In order to do so create a new file called subscriptions_extension.jag in the directory path <APP-HOME>/resources/repository/deployment/server/jaggeryapps/store/apis/ and add the code segment below.
    <%
    include('/jagg/jagg.jag');
    var storeHostObj = jagg.module("manager").getAppStoreExtensionObj();
    
    (function () {
        var mapper,
                config = require('/config/store.js').config(),
                mod = require('/modules/store.js'),
                file = require('/modules/file.js'),
                matcher = new URIMatcher(request.getRequestURI()),
                server = require('store').server,
                tenant = server.tenant(request, session),
                configs = mod.configs(tenant.tenantId);
        mapper = function (path) {
            return function () {
                return path;
            };
        };
        var method = request.getMethod();
        var log = new Log('subscriptions_extension : ');
    
        if (jagg.getUser() == null) {
            print({
                error: true,
                message: 'Authenticate error. Please login to the system.'
            });
        } else {
    
            var endPoint = 'subscriptionsExtension/{action}/{user}';
            var tenantedPageEndpoint = '/{context}/t/{domain}' + endPoint;
            var normalPageEndpoint = '/{context}/' + endPoint;
    
            if (matcher.match(normalPageEndpoint) || matcher.match(tenantedPageEndpoint)) {
                var elements = matcher.elements();
                var user = elements.user;
                var action = elements.action;
    
                if (method = 'GET') {
                    // get method to get the given user subscription details
                    if (action == 'userSubscriptions') {
                        try {
                            var subscriptions = storeHostObj.getSubscriptionsByUser(user);
                            print({
                                error: false, subscriptions: subscriptions
                            });
                        } catch (e) {
                            print({
                                error: true,
                                message: e.message
                            });
                        }
                    }
                }
            } else {
                print({
                    error: true,
                    message: 'Requested API is not found'
                });
            }
        }
    }());
    %>
    
    • Here, we get a object of the hostobject by calling the hostobject getSubscriptionsByUser(String user) method in the manager module. You need to call this inside the GET method.
    • Tip: (when extending the API with the same URL mapping) in this API definition, the context has an {action}. By using another action you can extend the same API for that action. For example, For the API call with the URL mapping https://localhost:9763/store/subscriptionsExtension/appSubscription, you can write you own code to get the subscribed user list for the particular application. You can also use any HTTP method inside this API.

Test the API

Now we can test the API to retrieve the user subscription data. First we need to login to the store app by using the username and password.

  • Login to the app store using cURL

    API call

    curl -c cookies -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"admin"}' https://localhost:9763/store/apis/user/login
    

    Response

    {"error" : false, "message" : "User admin successfully logged in"}
    
  • API invocation
    curl -b cookies -X GET https://localhost:9763/store/subscriptionsExtension/userSubscriptions/<username>
    

    e.g. API call

    curl -b cookies -X GET https://localhost:9763/store/subscriptionsExtension/userSubscriptions/admin
    

    Response

  • Response JSON
    Key Value Description
    error false API call was successful or not
    subscriptions [{subscription1}{subscription2}] Array of the application subscription for the given user
    subscriptionId 1 ID of the subscription - this will be the auto incremented value
    appId 1 ID of the application - this will usually be 1 since we use the default application
    appName app1 Web app name
    appVersion 1.0.0 Web app version
    appProvider admin Web app provider
    subscriptionTime Date time Subscription date and time for a particular app
    evaluationPeriod Date time Evaluation period for the appEvaluation period for the app
    expiredOn Evaluation period as hours Date and time of expiry for a particular app
    isPaid true/false Whether the user has paid or not for the particular app

Validate the subscription expiry

When the application is invoked, we have the following parameters for the user request.

  • User
  • Application name
  • Application version
  • Application provider

To validate the subscription we first need to call the API with the user to obtain the user-wise application subscriptions list. Then we can identify what application the user was requesting by using the above application specific parameters. Now we can validate the subscription expiry details for the particular application and take action accordingly.


Conclusion

This tutorial, is the second part of the two-part tutorial series. Here we discussed how to implement APIs in the WSO2 App Manager using Jaggery modules. You have your hostobject in the Java side, which contains the business logic and the API definition in the Jaggery side. By using a Jaggery module we can integrate those two and finally provide the API to the end user.


References

 

About Author

  • Sajith Abeywardhana
  • Software Engineer
  • WSO2