When Old Meets New
Joshua Close
SAP BTP Solutions Architect with Strong Development Background | Leading SAP Integration Expert
Finding creative and efficient ways to move forward with new SAP technologies in older SAP ECC Environments using a Full Stack approach.
Background
Often companies are faced with the dilemma of wanting to move forward with newer technologies while having older backend systems. This was the case in this scenario as the warehouses in our company use a custom developed aging suite of web-based applications to perform WM, IM, PP, SD, and MM activities within an SAP AFS ECC 6 environment. These transactions are executed within the four walls of the warehouse using a wireless network to connect numerous device types which now include different makes, models, operating systems, and screen sizes. The code base behind these transactions were written many years ago utilizing JAVA WebDynpro on a Standalone Java Netweaver stack and required little maintenance since this initial implementation until recently. At the time of the initial development of these transactions, it was done with a specific, standardized device type in mind, as SAP officered built in options in JAVA WebDynpro to access the scanner wedge and function keys directly on Intermec/Honeywell devices. With the change in business conditions and requirements as well as the multitudes of warehouse grade devices available today, a more modernized and robust software platform needed to be explored to support new requirements while supporting existing functionality on newer devices. In the below article I hope to outline how we were able to use newer technology within an aging and limited hosted ECC SAP environment to be able to successfully implement and deliver a robust solution for mobile warehouse transactions.
The Business Requirement
As the business explored and purchased more cost efficient devices to be used in the warehouse for mobile based transactions for both large screen application on vehicle mount devices as well as smaller handheld devices, we needed to find a solution that enabled functionality to seamlessly work across these new devices as well as the legacy devices.
The business also had a new development requirement for a mobile transaction set to log handling units too large for the automated conveyor system to handle. This is a multistep process:
1. Develop a mobile transaction that requires the user to scan a carton and tote barcode and validate that the carton was put into a tote assigned to a specific forwarding agent. This action needed to be logged in SAP for traceability.
2. That tote would then be loaded into the truck. This loading action needed to be validated via a mobile that the tote was being loaded into the correct truck by confirming that the seal and trailer number matched against the tote’s assigned forwarding agent via lookup in the SAP shipment. This action needed to be logged in SAP for traceability.
3. A SQL Server stored procedure needed to be executed against the PLC controller database to confirm the carton had been scanned and loaded mimicking the automated conveyor scans.
The Problem
This new transaction set was the main driver that determined it was not feasible or efficient to update the existing JAVA WebDynpro solution for the above requirements. It was determined best to move to a different technology platform while outlining a go forward strategy for mobile warehouse transaction development. Several roadblocks were present in our environment as we tried to integrate newer technologies with older systems:
1) The R/3 ECC instance is on an older version which does not offer support for RESTful Api’s or SAPUI5 with no upgrade planned soon. How would we be able to host and develop this in a cost-efficient and maintainable way?
2) The R/3 ECC instance is a global instance and requesting software components such as SAP NW Gateway to be installed can be a lengthy process and requires input from all regions. How do we communicate between SAPUI5 and the backend SAP system?
3) Cost constraints on implementing new instances in the hosted environment were prohibited.
4) Latency concerns of serving HTML and ODATA/JSON based traffic to/from the hosted data center in the UK. Where can we host this for highest throughput and most economically?
5) What IDE do we use for development as Webide was not available to us?
The Solution
Looking forward, we wanted to use SAPUI5 with hopes it can be ported to a future environment after an SAP upgrade. SAPUI5 provides support for many devices, web browsers, handles screen sizing well, and is a widely used and supported platform provided by SAP, and is the basis to the go forward UI strategy of FIORI. To overcome our system environment limitations in dealing with an older version of SAP, we decided to develop a hybrid architecture that would allow us to quickly pilot this new transaction set to the business users and prove out the new technology to move forward with further adaptation of more legacy code. The platform components chosen were:
1) SAP RFC enabled function modules to facilitate the selects and updates to and from SAP.
2) Utilize a JAVA Spring Boot application to build the Service side of this model to use SAPJCO to facilitate communication with SAP and subsequently serve RESTful/JSON based services for consumption by the SAPUI5 application. Using Java also made it easy to use JDBC to fulfil the requirement to call the SQL stored procedure in the PLC database. RESTful API/JSON is a lightweight protocol that easily integrates with SAPUI5 via JQuery/AJAX. Spring Boot is also used in the Business Application Studio on the SAP Cloud platform which makes it a good interim strategy. This is hosted on a local server in the warehouse to improve upon network latency between the mobile devices and SAP ECC.
3) Use a SAPUI5 based frontend hosted on a Tomcat 9 Server. This is hosted on a local server in the warehouse to improve upon network latency.
Overall Architecture of Solution
Interactions between UI5 MVC, REST, and RFC
Approach to User Acceptance of Design
Approaching users with new concepts and technologies can often cause some apprehension and reluctance to accept the solution. To incorporate the business early in the design process we decided to use SAP build to provide a tool to facilitate business feedback and collaboration in modeling, routing and design of the user interface. This tool provides a good environment to model screens to bring requirements to reality. By giving them access to the build.me application users gained more comfort in navigating the new application to see how it will look and feel while interactively being able to provide feedback and had a feeling of inclusion in the design process.
Once the look and feel of the application was decided upon, Build provides a nifty feature to download a skeleton of the project to be imported into Webide or Eclipse (with some work).
Backend Development – SAP R/3
Backend development was done in SAP by developing Remote-Enable Function modules with future thought that these can be exposed via the SAP Netweaver Gateway as OData once upgraded or the plugin installed.
Appropriate Setting to Enable a function module to be accessible via SAPJCO
For this solution the function modules are called by the Spring RESTful services by the SAPJCO connection. A function module was created corresponding to each GET/POST method within the SPRING REST application and separated into different function groups for the GETS and POSTS as displayed below. PUTS,DELETES,etc were not applicable for this project.
Definition of Function Groups/Function Modules broken our by Reads/Updates
To illustrate the integration flow we will use the login process as an example. As each user of the SAPUI5 application has a SAPECC logon, and to comply with SAP licensing with RFC based scenarios, the application uses a function module to authenticate user credentials against SAP. Below is the code that performs this. As more functionality is developed, authorizations will also be returned via this function.
All functions are structured similarly in terms of the exporting parameters that include the MESSAGES and indicator representing a success or failure, so that the REST service can pass this in a standard way to be handled by the front end SAPUI5 controllers receiving the JSON data through an $AJAX call.
Login Function Module showing how function execution was successful and message structure which will be ultimately handled by the frontend and displayed to the user.
The Bridge Between SAP and UI5 - RESTful API/JSON Service Developed with Spring Boot/Web
Java/Spring and SAPUI5 development was done in MyEclipse, an extension of the standard Eclipse environment. This environment provided built-in Maven and Spring boot functionality in order to develop and debug middleware web service application.
The Spring Web and Rest dependencies provide a robust solution in setting up the middleware web service application enabling it to accept and send JSON from POJO. The Spring Framework Maven dependencies that need to be included in the pom.xml are:
pom.xml dependency section:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jersey</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
The application is structure with packages as follows:
com.xxx.rest.rf.controllers
This packaged contains the REST Controller objects that handle the accept GET/POST requests from the front-end controllers. Using Spring annotations, the path and type of method is defined. Additionally, the input request parameters passed via the URL are defined as well as the return. The return objects are defined as POJO and converted to JSON by Spring. The communication to the R/3 function modules and subsequent data mapping to POJO’s are handled in the classes in the com.xxx.rest.sap.integration package.
LoginController.java:
package com.xxx.rest.rf.controllers; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.xxx.rest.rf.pojo.Login; import com.xxx.rest.rf.pojo.Ping; import com.xxx.rest.sap.integration.SAPFunctionCalls; @RestController public class LoginController { private final SAPFunctionCalls sapcall; private static final Logger logger = LogManager.getLogger(SAPFunctionCalls.class); public LoginController() { sapcall = new SAPFunctionCalls(); logger.info("SAPFunctionCalls constructor"); } @GetMapping("/login") public Login Login(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password ) { try { logger.info("Attempting login for:" + username); Login l = sapcall.Login(username, password); if(l.isValid()) { logger.info("Login Successful for:" + username); } else { logger.warn( "Login Successful for:" + username); logger.warn("With Error: " + l.getMessage() ); } return(l); } catch(Exception ee) { logger.error("Login Exception",ee ); ee.printStackTrace(); return new Login(username,"",false,ee.toString()); } } @GetMapping("/ping") public Ping ping() { try { Ping p = sapcall.Ping(); return p; } catch(Exception ee) { logger.error("Ping Exception",ee ); ee.printStackTrace(); return new Ping(false); } } }
The @RestController annotation lets the Spring Framework know this is a RESTful web service and will produce JSON for the results returned in the methods (in this case POJO's)
The @GetMapping annotation maps the methods to a corresponding url path.
Additionally, parameters can be specified in the method to pass via the url as defined above in the Login Method. Here username and password must be passed or the Spring Framework will return a HTTP 404.
com.xxx.rest.rf.pojo package
This package contains the POJO’s (Plain Old Java Objects) that model the returns from SAP that are sent via the REST service to the front-end controllers. These objects are populated based on the return from SAP and are similar in structure to the SAP exporting structure defined in the RFC. Future functionality we wish to implement to save code and efficiently implement more functionality into the application as we bring in the legacy functions is to provide dynamic mapping between SAP function returns and POJO’s. Here is an example POJO from our login method:
Login.java:
package com.xxx.rest.rf.pojo; public class Login { private final String username; private final String name; private final boolean valid; private final String message; /** * @return the username */ public Login(String username, String name, boolean valid,String message) { this.username = username; this.name = name; this.valid = valid; this.message = message; } public String getUsername() { return username; } /** * @return the name */ public String getName() { return name; } public String getMessage() { return message; } /** * @return the valid */ public boolean isValid() { return valid; } }
com.xxx.rest.rf.web package
This package contains the classes that initialize the Spring framework and setup various configurations within the Spring Boot framework. The main bootstrap is handled in the class with the @SpringBootApplication annotation:
RfrestApplication.java:
package com.xxx.rest.rf.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication public class RfrestApplication { public static void main(String[] args) { SpringApplication.run(RfrestApplication.class, args); } }
Additional security can be implemented and configured in a class annotated with the @EnableWebSecurity and use the CorsConfigurationSource. Here, the security definition is only allowing communication from incoming requests for the subnet our mobile warehouse devices are on. As these services provide data to requests from a specific subnet and is additionally protected by firewalls, other security was not implemented. Below is a definition of the security implemented:
WebSecurityConfig.java:
package com.xxx.rest.rf.web; import java.util.Arrays; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().configurationSource(corsConfigurationSource()); http.csrf().disable(); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("10.20.134.*")); configuration.setAllowedMethods(Arrays.asList("*")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.addAllowedHeader("Content-Type"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
com.xxx.rest.sap.integration package
This package contains the classes that facilitate the communication to SAP R/3 via SAPJCO. The connection configuration properties that define the backend connection are handled through a properties files that the SAPDestinationDataProvider consumes through the java Properties class. This class is used in the RESTController. Below is how the SAPJCO is used to call the backend ZMM_READ_LOGIN_DATA function module defined in SAP.
SAPFunctionCalls.java:
package com.xxx.rest.sap.integration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.xxx.rest.rf.pojo.BAPIRET2; import com.xxx.rest.rf.pojo.Carton; import com.xxx.rest.rf.pojo.Container; import com.xxx.rest.rf.pojo.Login; import com.xxx.rest.rf.pojo.Ping; import com.xxx.rest.rf.pojo.PostResults; import com.xxx.rest.rf.pojo.Seal; import com.xxx.rest.rf.pojo.Trailer; import com.sap.conn.jco.JCoDestination; import com.sap.conn.jco.JCoDestinationManager; import com.sap.conn.jco.JCoException; import com.sap.conn.jco.JCoFunction; import com.sap.conn.jco.JCoStructure; import com.sap.conn.jco.ext.Environment; public class SAPFunctionCalls { String DESTINATION_NAME2 = "ABAP_AS"; JCoDestination destination; static SAPDestinationDataProvider myProvider; private static final Logger logger = LogManager.getLogger(SAPFunctionCalls.class); public SAPFunctionCalls() { myProvider = SAPDestinationDataProvider.getInstance(); if (!Environment.isDestinationDataProviderRegistered()) { Environment.registerDestinationDataProvider(myProvider); } } public Login Login(String username, String Password) throws JCoException { try { destination = JCoDestinationManager.getDestination(DESTINATION_NAME2); destination.ping(); JCoFunction function = destination.getRepository().getFunction("ZWMM_READ_LOGIN_DATA"); function.getImportParameterList().setValue("UNAME", username); function.getImportParameterList().setValue("PWD", Password); function.execute(destination); JCoStructure msg = function.getExportParameterList().getStructure("MESSAGE"); String valid = function.getExportParameterList().getString("VALID"); boolean validb = false; if (valid.equalsIgnoreCase("X")) { validb = true; } else { validb = false; } String NAME = function.getExportParameterList().getString("NAME"); return (new Login(username, NAME, validb, msg.getString("MESSAGE"))); } catch (Exception ee) { logger.error("Login Error", ee); return (new Login(username, "", false, ee.toString())); } } }
MyEclipse can kick off the debug of the Spring Boot application. Once the application is running, unit testing the Spring code can be facilitated via SOAPUI or Postman. Below is an example test of the RESTful Service and the login method. These tools provide an easy way of unit testing and allow the results of the development to easily visualized before integrating them into the SAPUI5 project.
Frontend Development – SAPUI5
MyEclipse is the IDE used to develop the frontend and has enhanced javascript coding and debugging functionality built in. The HTML editing works well, however it does not have functionality for SAPUI5 tag autofill.
The frontend was scaffolded in SAP Build and imported into MyEclipse. Much work was needed to make this work, including defining routing, cleaning up of the views, and implementing controller and bootstrap code to consume the RESTful API and bind it to models. This IDE also has native functionality to deploy the code on a local Tomcat instance for testing and debugging.
The SAPUI5 project in MyEclipse is structured like it would in the SAP Webide for a WEB UI5 module.
Explaining the project, we can see the views and controllers which are the central objects of the project that allow interaction between the end user and the RESTful API
Bootstrapping
The main entry point into the web application is the index.html. This file contains the reference to the core ui5 library which handles the subsequent boot loading. It also contains various configurations including the theme of the site which determines the look and feel.
index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <header name = ""></header> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mobile WM</title> <script id="sap-ui-bootstrap" src="resources/sap-ui-core.js" data-sap-ui-libs="sap.m ,sap.ui.layout" data-sap-ui-xx-waitForTheme="true" data-sap-ui-theme="sap_fiori_3" data-sap-ui-language="en" data-sap-ui-xx-bindingSyntax="complex" data-sap-ui-resourceRoots='{ "sap.ui.demo.todo": "./" }' data-sap-ui-onInit="module:sap/ui/core/ComponentSupport" data-sap-ui-compatVersion="edge" data-sap-ui-async="true"> </script> </head> <body class="sapUiBody" id="content"> <div data-sap-ui-component data-name="sap.ui.demo.todo" data-id="container" data-settings='{"id" : "todo"}'></div> </body> </html>
The Manifest.json contains the main configuration of the application. This includes:
1) dataSources
2) Ui5 versions and dependencies
3) Models
4) initialize routing configuration that defines navigation between views.
The routing for this application calls the Login.view.xml as defined in the Manifest.json
...{ "pattern": "", "name": "default", "targetControl": "app", "target": ["Login"] ... } .... Login": { "controlAggregation": "pages", "viewName": "Login", "viewId": "Login", "viewLevel": 0, "transition": "slide" }
Logon.view.xml:
<mvc:XMLView xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" controllerName="sap.ui.demo.todo.controller.Login" async="true" xmlns="sap.m"> <App id="app"> <Page showHeader="true" title="Mobile WM Login" showNavButton="false"> <content> <VBox alignItems="Center" direction="Column" fitContainer="true" width="293px" height="215px" justifyContent="SpaceBetween" renderType="Div" visible="true" displayInline="false"> <items> <Title text="Enter Login Information" titleStyle="H3" width="auto" textAlign="Begin" visible="true" wrapping="false" /> <Text xmlns="sap.m" text="Username" id="text0" /> <Input xmlns="sap.m" value="" id="inputUsername" required="true" placeholder="Enter SAP User ID" /> <Text xmlns="sap.m" text="Password" id="text1" /> <Input xmlns="sap.m" value="" id="inputPassword" type="Password" placeholder="Enter password" required="true" submit="_onButtonPress" /> <Button text="Login" type="Default" iconFirst="true" width="183px" enabled="true" visible="true" iconDensityAware="false" press="_onButtonPress" /> </items> </VBox> </content> <footer id="appViewFooterId"> <core:Fragment fragmentName="sap.ui.demo.todo.view.Footer" type="XML" /> </footer> </Page> </App> </mvc:XMLView>
This page contains basic Layouts to organize the page as needed, input controls to accept username and password and a button to trigger the _OnButtonPress function in the Login.controller.js.
Login.controller.js:
_onButtonPress : function(oEvent) { var username = this.getView().byId("inputUsername").getValue(); var password = this.getView().byId("inputPassword").getValue(); var ok = false; var oModel = new sap.ui.model.json.JSONModel(); var oRouter = this.getRouter(); var input = this.getView().byId("strip0"); var comp = this.getOwnerComponent(); var url = Constants.BASE_URL; var view = this.getView(); if (username == "") { this.getView().byId("inputUsername").setValueState(sap.ui.core.ValueState.Error); ok = false; } else { ok = true; this.getView().byId("inputUsername").setValueState(sap.ui.core.ValueState.None); } if (password == "" && ok == true) { ok = false; this.getView().byId("inputPassword").setValueState(sap.ui.core.ValueState.Error); } else { ok = true; this.getView().byId("inputPassword").setValueState(sap.ui.core.ValueState.None); } if (ok == true) { $.ajax({ url : url + "/login?username=" + username + "&password=" + password, success : function(data, textStatus, jqXHR) { oModel.setData(data); comp.setModel(oModel, "loginData"); if (data.valid == true) { view.byId("inputUsername").setValue(""); view.byId("inputPassword").setValue(""); oRouter.navTo("Menu"); } else { view.byId("inputPassword").setValue(""); view.byId("inputPassword").focus(); MessageBox.error("Invalid Login: " + data.message.message); } }, error : function(xhr, status, error) { MessageBox.error("SAP Error: " + xhr.responseText); } }); } },
Some notes on what the controller code is doing in regards to _onButtonPress() function:
1) The $.ajax call uses JQuery to call the RESTful API created via Spring. It gets the URL from a constants file that can be changed for Dev/QA/Prod. It also passes in the required parameters of username and password.
2) 2 callback methods are defined:
a. success: if the services returns a valid http 200 response code, this method is fired. The JSON is passed back to the controller via the data parameter, is stored in a model for later access to the user's name and user id. The parameter is an object and can be accessed as object.field . A check is performed if the rest controller passed us back a valid flag which signals the login was successful. if not, it returns an error to the view as a MessageBox. If valid == true, then we use routing to move to the main menu.
b. error: if the service returns an error response code, we present this to the frontend as a MessageBox. This would only be returned if there is an issue with the backend processes.
All views use a common footer as defined in a Fragment. This footer consists of a message strip that is refreshed on a 15 second frequency by a function in the Base.controller that executes the ping RESTful API which ensures connectivity is established between the device, the API and the subsequent backend SAP system. This is useful in two regards:
1) In a warehouse environment network connectivity can be extremely transient due to interference by metal racking, etc. This ensures the device is in range of wireless.
2) The system ID is displayed. This ensures a user does not inadvertently pick up a test vs production device.
The resulting index.html displays in a browser as below:
Building and deploying to Tomcat
SAP Provides a build tool which can be triggered by the command line statement: UI5 build --all. When run in the project folder it will create dist folder as below that will contain everything to run the UI5 frontend from a tomcat instance. The resources folder contains the ui5 libraries for the specified version in the configuration file. The process further ‘uglifies’ the code removing spacing and line breaks to shrink the files to decrease loading times.
Dist folder created by build process
Overall Impressions
The application is in production and has been used on many mobile device types and sizes with much success. The speed on the wireless network to the local tomcat instance displays little latency and quick response times between barcode scans and screen interactions and processing. This pilot project has been a successful program proving this Full Stack approach to UI5 technology will work within a mobile WM environment and on the devices currently configured in that environment.
Next Steps
Several next steps are needed to further this application and it’s use in the warehouse.
1) Further bring over more functionality from the legacy Java WebDynpro environment
2) Implement authorizations based on SAP authorization model to limit access to the new functionality
3) Although limited to the warehouse wireless subnet now, increase security to include JSON Web Tokens (JWT) which is handled nicely by spring security
4) Implement i18n for texts within UI.
? 2021 Joshua Close All Rights Reserved
IT eCommerce Manager at Samsonite
4 年Well done, Josh! Thank you for the work that you do. ??
Results-Driven Senior Ecommerce Leader | Driving Profitable Growth, Cross-Functional Excellence, and Customer Satisfaction
4 年Great job, Josh!!
Managing Partner/Solution Architect at Eastern Analytics
4 年Great write up Josh! Your definitely the go-to guy for this stuff. It’s great working with you, I always learn a lot ??