March 1, 2007

sap4rails updated to support new sapnwrfc

sap4rails has been upgraded to support the new RFC Connector for Ruby - sapnwrfc.

Posted by PiersHarding at 9:39 AM

February 28, 2007

New RFC Connector - sapnwrfc

SAP has undertaken a project to re-engineer the RFC SDK (creating the SAP NetWeaver RFC SDK), which is good news for the Connectors (ref.).

This means that eventually - all the RFC connectors, and 3rd party products will need to be updated to use the new under-carriage.

So whats the big deal?

RFC is a stable technology, and has been for many years, so I can understand why this revelation may not seem very exciting. What is exciting is the unprecedented level of cooperation, understanding and good will that has come out in a relatively short time, as I have moved through the process of redeveloping the Ruby and Perl RFC Connectors. The result is (and will be more so), a better fit in terms of how the SDK works with Dynamic Languages, allowing the API that the Dynamic Languages offer for RFC connectivity, to better reflect the nature of those programming languages. For example - there are better features in the new NW RFC SDK that allow for easy translation of ABAP types to Ruby/Perl types.

New Features

If we set aside the rationalisation, and simplification of the NW SDK (which is a bonus in itself), there are new features of the NW SDK that can be drawn upon -

This has lead to a complete overhaul of the Ruby, and Perl Connectors, with the aim to take advantage of the new NW SDK features, and to produce connectors that create a more intuitive bond between the underlying RFC API, and the natural features of each Dynamic Language.

Call for testers

As the new Ruby and Perl RFC Connectors are a complete rewrite and Alpha, I am calling for testers/early adopters from the Community.

Obtaining the Connectors, and Netweaver RFC SDK

Download the new sapnwrfc connector for Ruby, and get the new RFC SDK, port your applications to it, and let me know how you get on. I'm interested in usability feedback, problems, and feature requests.

For Ruby - download from the RAA. Follow the instructions in the included README file. Documentation is available Here. A GEM install package for Win32 has been built available here.

For Perl - download from CPAN. Again - follow the instructions in the included README file. Documentation is available Here. A PPM install package for Win32 has been built available here.

If you are interested in trialling/testing the new connectors, then along with the installing the new connectors (above) you will need to obtain the new Netweaver RFC SDK. in order to do this, please register your interest with me, ensuring that I know how to contact you, and what your platform requirements are, and with the help of the NW RFC team at SAP I will get the relevant details to you.

Ruby Examples

There are plenty of examples in the tests/ directory of the sapnwrfc download, but here is a basic walk through of the new API:
 # specify a YAML base config file or pass connection
 #   parameters directly to rfc_connect()
 SAPNW::Base.config_location = './config_file.yml'
 SAPNW::Base.load_config
 conn = SAPNW::Base.rfc_connect

 # get the system and connection details
 attrib = conn.connection_attributes
 $stderr.print "Connection Attributes: #{attrib.inspect}\n"

 # lookup the dictionary definition of an Function Module
 fds = conn.discover("STFC_DEEP_STRUCTURE")
 $stderr.print "Parameters: #{fds.parameters.keys.inspect}\n"

 # create an instance of a Function call
 fs = fds.new_function_call

 # populate the parameters - structures and table rows now take hashes of field name/value pairs
 fs.IMPORTSTRUCT = { 'I' => 123,
                     'C' => 'AbCdEf',
                     'STR' =>  'The quick brown fox ...',
                     'XSTR' => ["deadbeef"].pack("H*") }

 # execute the RFC call
 fs.invoke
 $stderr.print "RESPTEXT: #{fs.RESPTEXT.inspect}\n"
 $stderr.print "ECHOSTRUCT: #{fs.ECHOSTRUCT.inspect}\n"

Config file (refer to the sap.yml file in the download):

ashost: ubuntu.local.net
sysnr: "01"
client: "001"
user: developer
passwd: developer
lang: EN
trace: 2

Test it out - and give your feedback.

Perl Examples

As with Ruby, there are plenty of examples in the software download in the t/* directory. Again - here is a taster showing the new API:
 use sapnwrfc;
 use Data::Dumper;

 # specify a YAML base config file or pass connection
 #   parameters directly to rfc_connect()
 SAPNW::Rfc->load_config;
 my $conn = SAPNW::Rfc->rfc_connect;

 # lookup the Function Module
 my $fds = $conn->function_lookup("STFC_DEEP_STRUCTURE");

 # initialise a call instance
 my $fs = $fds->create_function_call;

 # set the parameters
 $fs->IMPORTSTRUCT({ 'I' => 123,
                     'C' => 'AbCdEf',
                     'STR' =>  'The quick brown fox ...',
                     'XSTR' => pack("H*", "deadbeef")});

 # invoke the Function Module and then play with the results
 $fs->invoke;
 $stderr.print "RESPTEXT: #{fs.RESPTEXT.inspect}\n"
 $stderr.print "ECHOSTRUCT: #{fs.ECHOSTRUCT.inspect}\n"
 print STDERR "RESPTEXT: ".Dumper($fs->RESPTEXT)."\n";
    ok($c eq 'AbCdEf');
 print STDERR "ECHOSTRUCT: ".Dumper($fs->ECHOSTRUCT)."\n";

 # cleanup
 $conn->disconnect;

Config file format is the same as for Ruby - refer to the sap.yml file in the download:

ashost: ubuntu.local.net
sysnr: "01"
client: "001"
user: developer
passwd: developer
lang: EN
trace: 2

Test it out - and give your feedback. The best place would be to carry on the discussion through the Forums.

Special thanks go to:

Notes/Updates:

See the download instructions for the SAP NW RFCSDK here.

Posted by PiersHarding at 12:28 PM

November 11, 2006

RFC_STRING and RFC_XSTRING type support for saprfc for Ruby

Support for string types now available in saprfc for Ruby from version 1.52. This opens the way for interaction with RFC calls that require variable length storage eg. true strings in either character or binary form. This was particularily useful for manipulating logon tickets, as shown by this example:

isusr = rfc.discover("SUSR_CHECK_LOGON_DATA")
isusr.AUTH_METHOD.value = "E"
isusr.AUTH_DATA.value = "p:ompka\\piers"
isusr.EXTID_TYPE.value = "NT"
rfc.call(isusr)
puts "RESULT: "

# access an interface parameter value
print "TICKET:             #{isusr.TICKET.value.to_s}\n"
ticket = isusr.TICKET.value.to_s

rfc2 = SAP::Rfc.new(:ashost => "192.168.1.2",
                   :sysnr  => 00,
                   :lang   => "EN",
                   :client => "010",
                   :mysapsso2 => ticket,
                   :trace => 1
       )

This code snippet demonstrates using one RFC connection to generate login tickets for another - a very useful trick for brokering connections within an external application. The login tickets in the standard RFC are carried in an RFC_STRING (isusr.TICKET) type parameter.

Note: Thanks to Gregor for pointing this out.

Posted by PiersHarding at 11:00 AM

November 2, 2006

Repensando a web com Rails

Fabio Akita has just contacted me to say that his new book "Repensando a web com Rails" has been released for the Brazilian market. The book is primarily about Ruby on Rails, but contains a section on SAP integration with Rails, which I helped (a little) with
Congratualtions Fabio - Hope it sells well.

Posted by PiersHarding at 1:27 PM

October 31, 2006

Ruby and the SOAP RunTime (SRT) Handler

Following on from my previous post about Ruby, Ruby on Rails, and SAP Web Services Integration - I would like to show how to switch to using the SOAP RunTime (SRT) Handler, which makes available SAP Web Services via Virtual Interfaces.

Steps

Create the SAP Web Service

We need to create a web service that exposes the function modules that were used in the UserAdmin Rails application. To do this - go to transaction SE80, go to the Enterprise Services tab, and create a Virtual Interface of type Funciton Group called Z_USERADMIN. Include into this the function modules:

Activate this, and then create a Web Services definiton - again, using SE80, go to the Enterprise Services tab, and create a Web Services definition called Z_USERADMIN - referencing the previously activated Vitual Interface Z_USERADMIN.
Finally - activate this in the ICF configuration (SICF), by using transaction WSCONFIG, referencing the Web Service definition Z_USERADMIN created above.
There is an excellent discussion of the details of this process by Thomas Jung, here, and here.

Modify model sap_user.rb

Now for the final part - in the UserAdmin Rails application, edit the SapUser model file app/models/sap_user.rb. This needs to switch from referencing the method "function_module" for loading the functions, to using "resources", as outlined below:
require_gem "sap4rails"

class SapUser < SAP4Rails::WS::Base

# You must define a list of RESOURCES to preload
   resources "http://seahorse.local.net:8000/sap/bc/srt/rfc/sap/Z_USERADMIN"
...
Now you can test it as in the previous weblogs.

Unicode!

One thing that I neglected to say in my previous post, is a major advantage of using sapwas for accessing SAP is that it has comprehensive Unicode support - free of charge (but not of pain) :-) .

Round Up

For me - this rounds up SAP Web Services and Ruby - you can either access them in Ruby directly by using the library sapwas, or taking advantage of the Rails integration provided by sap4rails.

Posted by PiersHarding at 11:12 AM

October 23, 2006

sap4rails 0.07 - SAP RFC and auto-reconnect

I released sap4rails 0.07 this week, which corrects a problem with RFC connections not reconnecting correctly (this is using saprfc as the driver - there is no similar issue wtih the sapwas driver, which uses stateless HTTP). This is especially important when the R/3 (ABAP) application server your Rails application is connected to goes down temporarily (or indeed any other temporary communication disturbance). Before an RFC call is executed, the conection is ping checked, and then a single reconnection attempt is tried if the ping fails. If the reconnect fails then an error is raised so it is a good idea to wrap your code in a rescue block, to deal with the situation in your local conditions. Here is an example log:

...
[SapUser] missing_method: Z_BAPI_USER_GETLIST
[SapUser] ifaces: ["SUSR_USER_LOCKSTATE_GET", "BAPI_USER_GET_DETAIL", "BAPI_USER_UNLOCK", "BAPI_USER_LOCK", "Z_BAPI_USER_GETLIST"]
in check connect...
Think Im connected - lets check ...
RFC PING[]...
Something wrong with connection(1) - do it again ...
new connection(2) ...
allready connected ...
[SapUser] missing_method: Z_BAPI_USER_GETLIST
[SapUser] ifaces: ["SUSR_USER_LOCKSTATE_GET", "BAPI_USER_GET_DETAIL", "BAPI_USER_UNLOCK", "BAPI_USER_LOCK", "Z_BAPI_USER_GETLIST"]
in check connect...
Think Im connected - lets check ...
RFC PING[true]...
...

Here you can see the first RFC_PING return nil, and the second after reconnect return [true].

Posted by PiersHarding at 3:05 PM

October 18, 2006

sapwas for Ruby and HTTPS

sapwas for Ruby now enables SAP Web Services to be called via HTTPS. This first requires you to setup SSL support in NW4 - which isn't too much trouble if you follow Gregors' excellent advice here. Once that is in place, then it is just a matter of structuring the URL coorectly, as described in this example:

require "SAP/WAS"

@was = SAP::WAS.new(:url => "https://seahorse.local.net:8443/sap/bc/srt/rfc/sap/Z_RFC_READ_REPORT_01",
                    :lang   => "EN",
                    :client => "010",
                    :user   => "developer",
                    :passwd => "developer",
                    :trace  => true)

# get a list of users
irep = @was.RFC_READ_REPORT
irep.program.value = 'SAPLGRAP'
irep.call()
puts "qtab no rows are: #{irep.qtab.rows.length.to_s}"
puts "qtab rows are: #{irep.qtab.rows.inspect}"

sapwas fully integrates with sap4rails as an alternative driver for accessing either RFCs or SAP SOAP based Web Services, and is available here.

Posted by PiersHarding at 10:48 AM

October 15, 2006

Ruby, Ruby on Rails, and SAP Web Services Integration

Something I like about Scripting Languages is the way they revel in having "more than one way to skin a cat". So, in this spirit I have built a complementary interface to saprfc (for Ruby) called sapwas, that facilitates RFC calls via SAP Web Services. This has been integrated into sap4rails, and the attached example demonstrates how to substitue Web Services for RFC integration in Ruby on Rails.

SAP Web Services vs RFC?

Other than curiosity, there is another motivation for trying this out - Since there has been discussion of late on the merits of RFC and Web Service technology such as SOAP, I thought that inorder to do the subject justice I would do some further (tangible) investigation.

To me, the most obvious way to draw a comparison, is to develop comparable examples of each, with the only difference being the substitution of the technology in question.

This led to me focusing on a recent article I wrote: Ruby on Rails with AJAX, to use as a base line.

Activating the SAP Web Service support

If we take the example above (Ruby on Rails with AJAX), then the changes are as follows:

Install sapwas

Download either the source distribution, or the gem file by following the sapwas project download links. Gem files are easier to deal with - all you need to do is:

gem install sapwas-<version>.gem

And its done (use gem uninstall to remove if its there allready).

You will probably need to install http-access2 inorder to get the basic authentication working correctly. This is a requirement of the SOAP4R library.

Upgrade sap4rails

Download either the source distribution, or the gem file for sap4rails by following the project download links. First remove the old version, and install the new:

gem uninstall sap4rails
gem install sap4rails-<version>.gem

If you have used the source distribution previously, then you will have to manually remove the previous one, and install again (the usual ruby setup.rb dance - gems are soo much easier :-).

Modify the SapUser model

In the Rails application, edit the app/models/sap_user.rb, and change the super class for SapUser:

class SapUser < SAP4Rails::WS::Base
...

Set your Rails configuration

Modify the config/sap.yml file - you really should only need to add in the :url line depicted below, but check anyway:

  client: "010"
  url: "http://seahorse.local.net:8000/sap/bc/soap/rfc"
  user: developer
  passwd: developer
  lang: EN

note on WAS configuration

You must ensure that /sap/bc/soap/rfc is configured/activated correctly in transaction SICF.

Go!

Once you have completed these changes, you can then test the Rails application as described in Ruby on Rails with AJAX - it should function in exactly the same manner - wasn't that easy!

Now you have it working - I'll leave you to draw your own conclussions on it's merits :-)

Posted by PiersHarding at 6:11 PM

October 12, 2006

Changing parameters in saprfc for Ruby

saprfc for Ruby now has the ability to pass Changing type parameters. These are an evolution of import, and export parameter types rolled into one. this functionality is available from release 0.31. This example shows the standard test RFC where COUNTER is a changing type parameter - the value is incremented on each pass:

iter = 10
rfc = SAP::Rfc.new(...)
# look up the interface definition for an RFC
i = rfc.discover("STFC_CHANGING")
i.counter.value = 0
iter.times {|cnt|
  puts "\n\n\n\n\nITERATION #{cnt + 1}\n\n\n\n"
  i.start_value.value = cnt
  rfc.call(i)
  puts "RESULT: #{i.result.value}  COUNTER: #{i.counter.value}"
}
# close the connection
print "close connection: ", rfc.close(), "\n"

Posted by PiersHarding at 6:15 AM

October 10, 2006

SAP the Enterprise, and Rails interest is building

I've just recently had contact from Lance, at the Engine Yard, who specialise in Rails applicaition development, and Hosting. These guys recognise that for their more Enterprise related clients, there is likely to be an SAP related dimension to their requirements.

To me - this kind of interest shows that Ruby on Rails is making a step change in it's progress through the world of IT, where it will likely start penetrating into the Enterprise software development market.

Posted by PiersHarding at 7:58 AM

October 6, 2006

IM for SAP with Jabber (XMPP)

Jabber and IM is very much in the ascendancy at the moment, thanks to Google Talk which uses the very same XMPP protocol for messaging.

With this in mind I would like to demonstrate how to easily integrate at least some minimal IM capability into SAP, being able to pass basic messages back and forth between IM accounts and R/3 accounts, and then to touch on the potential for IM beyond this.

A bit of background

XMPP

Is an XML streaming protocols for instant messaging and presence developed within the Jabber community. Because the transmission of data is encapsulated in XML, and must conform to the controlling rules of XML, coupled with implementation rules for the protocol such as dialback for server to server communication etc., making XMPP a very secure messaging platform. Testimony to this is the absence of SPAM, in fact it could be robustly argued that if the backbone of SMTP was replaced with XMPP then SPAM would be history (but that is for another day).
Additionally, XMPP is a recognised Standard - the Internet Engineering Task Force (IETF) has formalized the core XML streaming protocols as an approved instant messaging and presence technology under the name of XMPP, and the XMPP specifications have been published as RFC 3920 and RFC 3921.

Jabber

being the historical root of XMPP, has a number of server, client and component implementations surrounding it. In brief, a Jabber environment requires a: For a more indepth ovreview, then this from the JSF is a good starting point.

Jabber and SAP

For my trial implementation, I didn't want to change an R/3 system in anyway, so the logical solution is to start with what SAP has to offer by way of a messaging interface, namely the BCS (Business Communication Service). It is concievably quite easy to modify the BCS to include a new service class along side SMTP, SMS, Faxing etc., but inorder to keep this as "zero modification", I have decided against that route.

The basic solution design is to use email on the R/3 side that is to be bi-directionally translated to Jabber based IM via a custom built XMPP transport component (SMTP Component pictured below).

image

Flow of messages between R/3 and Jabber Server.

Message Flow and Addresses

Because the messages are moving between two protocols, it is necessary to translate the from and to addresses between them, as the messages move through the SMTP transport Component. This is so that a message can find its correct XMPP destination (R/3 -> Jabber), and that the IM Message can be stamped with the appropriate From address so that when a reply is made, it will be able to find it's way back again (Jabber -> R/3).

image

Configure SAPConnect for SMTP

SMTP inbound and outbound must be configured in R/3. There is a good discussion of this in the SAP help here. In the following section I have highlighted the main parts of the configuration that I used to get my SAP Netweaver NW4 evaluation system working - depending on your release your mileage may vary.

Profile Parameters

Set your profile parameters:
   rdisp/start_icman = true
   icm/server_port_2 = PROT=SMTP,PORT=25000,TIMEOUT=180         
   is/SMTP/virt_host_0 = *:25000;                               

SICF Virtual Host configuration

Configure your ICM via transaction SICF:

image
Ensure that your SAPConnect Virtual Host Data is configured for SMTP.

image
On the Service Data tab, ensure that the R/3 user for email receipt is configured (including client, language etc.).

SCOT Configuration

image
In transaction SCOT, select the menu option for default domain - set this as the host receiving the email.

image
In transaction SCOT, you can optionally specify that recipt email is not expected. This makes it tidier in terms of the monitoring view of the send process.

image
Ensure that the SMTP node is configured - it usually uses the local OS transport mechanism such as sendmail (connecting to localhost port 25), and that the address range that will be sent to has a routing table rule in place. Make sure that the job that triggers the send process has been scheduled (SCOT => Settings => Send Jobs) - this is what shuffles the emails out on a periodic basis.

Finally - make sure that every R/3 account is configured correctly with an Internet Email Address. This is found in SU01, under address data, and communication methods.

The Jabber Component

Now that we have R/3 configured, we can move on to the Jabber side of things. I have created a transport component, that implements a simple gateway between XMPP, and SMTP. The component has been developed using Ruby and XMPP4R. By nature, a transport component has two main parts to it. It has the portion that implements the XMPP protocol, and the second part that implements the target protocol - in this case SMTP. There are other SMTP component implementation available such as smtp-t, but - inorder to simplify the sender and receiver addresses (See diagram above and below showing process flow and address translation) - I have decided to "roll my own". XMPP4R is easy to use, and taps into Rubys simple threads implementation - something that is essential for developing components that have two event cycles - listiening for SMTP messages on ones side, and XMPP on the the other.

In my landscape I have a Netweaver R/3 instance with an SMTP interface named seahorse.local.net listening on 2500. My Jabber SMTP component has an SMTP interface of sapsmtp.local.net listening on port 25, and this is registered with the XMPP router called gecko.local.net. As both SAP, and the Component have to have both sending and receiving capabilities for SMTP, and this is all configurable, I've tried to ease this pain with a diagram:

image

Make sure all your designated hostnames are resolvable - this is a common mistake with Jabber.

To run the component you must have installed Ruby and installed the XMPP4R package. This also assumes that you have access to a Jabber server - I use jabberd2, but there are a number available. You can find a good list here. ejabberd is good universal one, especially if you need to run win32.

Once you have access to your server, and have XMPP4R installed, download the component from here. Unpack the .tgz file, and then cd into the smtpcomp/ directory. Here, you need to know the addresses, and port numbers of your landscape to convert them into command line options:

piers@gecko:~/code/ruby/smtpcomp$ ruby smtpcomponent.rb -h
        Usage: smtpcomponent.rb [-h -s -p -a -c -r -q]
        Options
          -h this help
          -s Jabber server router jid (gecko.local.net) - target Jabber server
          -p Jabber server router port (5347) - target Jabber server port for components
          -a Jabber server router auth phrase (secret) 
          -c Jabber component jid and smtpd name (sapsmtp.local.net) - name of THIS component
          -r SAP system smtpd name (seahorse.local.net) - SMTP host of SAP
          -q SAP system smtpd port (2500) - SMTP port of SAP
  
piers@gecko:~/code/ruby/smtpcomp$ 

For my setup where I have a Jabber server, gecko.local.net, a component name of sapsmtp.local.net, and an SAP system seahorse.local.net with an SMTP service on port 2500, then the command line options operate like:

piers@gecko:~/code/ruby/smtpcomp$ sudo ruby smtpcomponent.rb -s gecko.local.net -c sapsmtp.local.net -r seahorse.local.net -q 2500
starting component at: sapsmtp.local.net
SMTPD going to listen on: sapsmtp.local.net/25
gecko.local.net http://jabber.org/protocol/disco#info to sapsmtp.local.net

Note: the use of sudo at the beginning of the command is to make the script run with root priveledges, as the SMTP interface of smtpcomponent.rb listens on port 25 which requires super user access. Also - you may need to specify the port for the component to connect to the router on (-p) and a password for authenticating with the route (-a).

What happens?

To show what happens, the following transcript traces first an R/3 email -> Jabber flow, and then the reverse. In this example, even though the sending R/3 user account is DEVELOPER, I have configured the INET mail address of the R/3 user to be piers@seahorse.local.net, in the user communication section of address details (transaction SU01).

This log section shows the SMTP transcript, the translation of addresses, and then the final construction of an IM message (in XML).

COMM: EHLO seahorse.local.net

COMM: MAIL From:

DID a 250 for MAIL 
COMM: RCPT To:

DID a 250 for RCPT 
COMM: DATA

Message: Is it ready yet!I've been waiting all day blah blah blah ....

DID a 250 for data 
COMM: QUIT

An email was created in R/3 by going to the SAP Office assistant (transaction SO01).

image

The IM Message arrives...

image

Now for the return leg using reply to the original IM Message, which is translated back through the SMTP component and sent to R/3.

Message from piers@gecko.local.net/Psi to piers@sapsmtp.local.net () subject: Re: Is it ready yet! : "no - stop pestering me"

image

Email in R/3

image

So what does this all mean?

What we have here is the ability to push events between R/3, and "A" another endpoint. By virtue of transport components, these events can traverse protocols, and can be integrated with just about any platform you care to think of because of client programming language support for XMPP (C, Java, Perl, Python, Ruby, Erlang to name some). To me, this all spells a universal messaging platform that is open, reliable, secure, standards compliant, that is ready to be used as a carrier for business data from alerts, to documents, to workflow events. It is worth taking a look at the Jabber Protocols page, which describes what processes, and functionality is currently supported, and what things are in the pipeline.

Currently Jabber clients routinely handle URLs, which make a good starting point for relaying Workflow items (integration points for BSPs, EP etc.). It also has specifications for RPC style encapsulation, and reference implementations for SOAP document transmission. Beyond this, Jabber messaging has the potential for embedding workflow objects ala Duet, to be interpreted by an extension to an existing Jabber client, and I haven't even started on mobile technologies yet.

Please note that I have quoted extensively from documentation supplied by the JSF throughout, and wish to thank them for that and their continuing efforts in developing, and promoting Jabber and XMPP.

Posted by PiersHarding at 11:10 AM

September 29, 2006

saprfc Ruby on Rails with AJAX

Since I wrote an article about integration of SAP and Ruby on Rails, it has been great to see the beginnings of a kernel of interest. As a result I decided to package up the sap4rails code and distribute it properly on RAA.

Rails and AJAX

One of the interesting things that Ruby on Rails provides is built AJAX functionality by virtue of an API over prototype, and Scriptalicious. In this blog, I would like to show how neatly this integration is implemented in RubyOnRails, using a simple example of Locking, and Unlocking SAP R/3 user accounts.

Installation Requirements

For this example you will need to do your own install of Ruby On Rails. Use the instructions for installing Ruby, Ruby On Rails, the RFCSDK, and saprfc in the RadRails blog.
Just be sure to install versions Ruby 1.8.4+ and Rails 1.1.2+.
Once you have got this far, the last thing to install is sap4rails. You can either install the source package or download the gem, and install this with:
gem install sap4rails-<version>.gem
.

UserAdmin

For this example, most of the standard BAPIs are adequate. We need to be able to list users, with their details, including their lock state. We also need to be able to lock and unlock them.
The general functionality of the application is to create two lists of users on a page - locked and unlocked - and for you to be able to drag a user from one to the other to change their lock state in SAP.

The following RFCs are used:

BAPI_USER_GETLIST is not quite enough. This, I have had to wrap in another function module and also modify the results table to include the lock status information of users.
Create a new function module called Z_BAPI_USER_GETLIST, and make sure that you activate it for RFC on the attributes tab (in SE37) (code).
Create a new structure called ZBAPIUSNAME, and include the two structures BAPIUSNAME, and USLOCK like this .
Make sure that you activate the structure.


The code and interface needs to be completed like this:

FUNCTION Z_BAPI_USER_GETLIST.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  IMPORTING
*"     VALUE(MAX_ROWS) TYPE  BAPIUSMISC-BAPIMAXROW DEFAULT 0
*"     VALUE(WITH_USERNAME) TYPE  BAPIUSMISC-WITH_NAME DEFAULT SPACE
*"  EXPORTING
*"     VALUE(ROWS) TYPE  BAPIUSMISC-BAPIROWS
*"  TABLES
*"      SELECTION_RANGE STRUCTURE  BAPIUSSRGE OPTIONAL
*"      SELECTION_EXP STRUCTURE  BAPIUSSEXP OPTIONAL
*"      USERLISTLOCK STRUCTURE  ZBAPIUSNAME OPTIONAL
*"      RETURN STRUCTURE  BAPIRET2 OPTIONAL
*"----------------------------------------------------------------------
*
data:
  LOCKSTATE LIKE  USLOCK,
  userlist like bapiusname occurs 0 with header line.

  refresh userlistlock.


  CALL FUNCTION 'BAPI_USER_GETLIST'
    EXPORTING
      MAX_ROWS              = 0
      WITH_USERNAME         = with_username
    IMPORTING
      ROWS                  = rows
    TABLES
      SELECTION_RANGE       = selection_range
      SELECTION_EXP         = selection_exp
      USERLIST              = userlist
      RETURN                = return
            .

  loop at userlist.
    move-corresponding: userlist to userlistlock.
    if userlistlock-firstname = space.
       userlistlock-firstname = userlistlock-username.
    endif.

    CALL FUNCTION 'SUSR_USER_LOCKSTATE_GET'
      EXPORTING
        USER_NAME                 = userlist-username
      IMPORTING
        LOCKSTATE                 = lockstate
      EXCEPTIONS
        USER_NAME_NOT_EXIST       = 1
        OTHERS                    = 2
              .

    move-corresponding: lockstate to userlistlock.
    append userlistlock.

  endloop.

ENDFUNCTION.

Activate the function module and test it.

The Rails part

The full application can be downloaded from here - but what I'd like to do is quickly describe the meat of what had to be done to get this type of application working.

Config - sap.yml

As described in the RadRails blog, you need to adjust the configuration in config/sap.yml to point to your SAP system:

development:
  ashost: 10.1.1.1
  sysnr: "00"
  client: "010"
  user: developer
  passwd: developer
  lang: EN
  trace: "1"
...

Model - sap_user.rb

The SapUser object now inherits from the new SAP4Rails::Base class. This serves to automatically take care of managing RFC connections based on the config done above. The two main class methods for use are function_module, which allows you to declare what RFCs you want to use, and parameter which is a helper method for declaring attributes of a SapUser (or any other Model object).
In the interests of simplifying the application, by reducing the amount of ABAP code to be written, and the number of RFC calls to be made, I have in a way "cheated", with the arrangement of methods defined in SapUser. Instead of having a SapUser#find method, I rely on the use of SapUser#find_all and SapUser#find_cache to reduce a series of SapUser searches down to one RFC call only. In reality this is probably not good practice, but it suits for this example.

Read the code comments below for further details:

require_gem "sap4rails"

class SapUser < SAP4Rails::Base

# You must define a list of RFCs to preload
  function_module :Z_BAPI_USER_GETLIST,
	                :BAPI_USER_LOCK,
	                :BAPI_USER_UNLOCK

# You must define a list of attribute accessors to preload
  parameter :last, :first, :userid, :locked

# do your attribute initialisation for each SapUser instance
	def initialize(last, first, userid, locked)
	  @last = last
	  @first = first
	  @userid = userid
	  @locked = locked
	  @changed = false
	end

# what is the lock state
	def locked?
	  return self.locked ? true : false
	end

# on #save - flip the lock state of the SapUser, calling the 
# appropriate RFC to do it
  def save()
    RAILS_DEFAULT_LOGGER.warn("[SapUser]#save what did we get: " + self.inspect)
   
    if self.locked?
      SapUser.BAPI_USER_LOCK.reset()
      SapUser.BAPI_USER_LOCK.username.value = self.userid
      SapUser.BAPI_USER_LOCK.call()
    else
      SapUser.BAPI_USER_UNLOCK.reset()
      SapUser.BAPI_USER_UNLOCK.username.value = self.userid
      SapUser.BAPI_USER_UNLOCK.call()
    end

    # just so something happens ...
    return true
  end

# one RFC call to get them all
  def self.find_all
    RAILS_DEFAULT_LOGGER.warn("[SapUser]#find_all ")
    SapUser.Z_BAPI_USER_GETLIST.reset()
    SapUser.Z_BAPI_USER_GETLIST.with_username.value = 'X'
    SapUser.Z_BAPI_USER_GETLIST.call()
    users = []
    SapUser.Z_BAPI_USER_GETLIST.userlistlock.rows().each {|row|
		  next if row['FIRSTNAME'].strip.length == 0
		  state = nil
		  if row['WRNG_LOGON'] == "L" || 
                     row['LOCAL_LOCK'] == "L" || 
                     row['GLOB_LOCK'] == "L"
		    state = true
	          else
		    state = false
		  end
		  users.push(SapUser.new(row['LASTNAME'], 
			                 row['FIRSTNAME'], 
				         row['USERNAME'], 
					 state))
		}
    return users
  end

# find a user base on the results of a SapUser#find_all
  def self.find_cache(user, cache)
    RAILS_DEFAULT_LOGGER.warn("[SapUser]#find_cache: #{user} ")
    cache.each{|row|
	  return row if user.strip == row.userid.strip
    }
  end

# get a list of all the locked users
  def self.find_locked
    RAILS_DEFAULT_LOGGER.warn("[SapUser]#find_locked ")
    locked = []
    find_all().each{|user|
	  locked.push(user) if user.locked
    }
    return locked
  end

# get a list of all the unlocked users
  def self.find_unlocked
    RAILS_DEFAULT_LOGGER.warn("[SapUser]#find_unlocked ")
    unlocked = []
    find_all().each{|user|
	  unlocked.push(user) unless user.locked
    }
    return unlocked
  end
end

Controller - lock_controller.rb

There are only 3 basic actions to the only controller in this application. The initial list action, build the starting page presenting the two lists of users (locked and unlocked). From there, as a result of the AJAX enabled calls from the dragndrop feature, two further actions are called - set_locked and set_unlocked.


class LockController < ApplicationController

# gnerate the starting user lists, and hand off to the default list view
  def list
    RAILS_DEFAULT_LOGGER.warn("[LIST] Parameters: " + @params.inspect)
    @locked_users = SapUser.find_locked()
    @unlocked_users = SapUser.find_unlocked()
    RAILS_DEFAULT_LOGGER.warn("[LIST] of Locked: " + @locked_users.inspect)
    RAILS_DEFAULT_LOGGER.warn("[LIST] of UNLocked: " + @unlocked_users.inspect)
  end

# check through the list of locked users in the locked sortable_element box
# and set their locked state in necessary
# on completion - render the partial locked_users
  def set_locked
    RAILS_DEFAULT_LOGGER.warn("[SET_LOCKED] Parameters: " + @params.inspect)
    @locked_users = []
    cache = SapUser.find_all()
    if @params['locked_box']
      @params['locked_box'].each {|locked|
	  next if locked.length == 0
          user = SapUser.find_cache(locked, cache)
	  next if user.first.strip.length == 0
	  if user && ! user.locked?
  	    user.locked = !user.locked?
   	    user.save
          end
          @locked_users.push(user)
      }
    end
    render :partial => 'locked_users', :object => locked_users
  end

# exact opposite/the same as set_locked
  def set_unlocked
    RAILS_DEFAULT_LOGGER.warn("[SET_UNLOCKED] Parameters: " + @params.inspect)
    @unlocked_users = []
    cache = SapUser.find_all()
    if @params['unlocked_box']
      @params['unlocked_box'].each {|unlocked|
	  next unless unlocked.length > 0
	  user = SapUser.find_cache(unlocked, cache)
	  next if user.first.strip.length == 0
	  if user && user.locked?
	    user.locked = !user.locked?
  	    user.save
  	  end
	  @unlocked_users.push(user)
      }
    end
    render :partial => 'unlocked_users', :object => unlocked_users
  end

# called by the rendering action of the partial locked_users
  def locked_users
    RAILS_DEFAULT_LOGGER.warn("[LOCKED_USERS] of Locked: " + @locked_users.inspect)
    @locked_users
  end

# called by the rendering action of the partial unlocked_users
  def unlocked_users
    RAILS_DEFAULT_LOGGER.warn("[UNLOCKED_USERS] of UNLocked: " + @unlocked_users.inspect)
    @unlocked_users
  end

end

Views

The the overall page template (layout) defines the shape of the page, and what JavaScript libraries are pulled in for the effects (AJAX). All pages inherit from this.

layout/application.rhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <title>User Administration:  <%= controller.controller_name %></title>
    <meta http-equiv="imagetoolbar" content="no" />
    <%= stylesheet_link_tag "administration.css" %>
    <%= javascript_include_tag "prototype", "effects", "dragdrop",
                       "controls" %>
  </head>

  <body>
  <div id="container">
    <div id="header">
    <div id="info">
      <%= link_to "home", :controller=> "/lock", :action => 'list' %>
    </div>
      <h1><%= link_to "UserAdmin - #{controller.controller_name}", :controller => "/lock" %>
        </h1>
    </div>
    <div id="content">
      <h2><%= @page_heading %></h2>
      <%= @content_for_layout %>
    </div>
  </div>
  </body>
</html>

lock/list.rhtml

The two most important things in list are the two container div tags - unlocked_box and locked_box. These in turn, have a corresponding partial (unlocked_users, and locked_users), that are responsible for generating the dragable user items.

<% @heading = "User Admin - Lock/UnLock" %>

  <div id="user-admin">
    <div id="unlocked" class="dropbox">
      <h3>Unlocked Users</h3>
      <div id="unlocked_box">
        <%= render :partial => 'unlocked_users', :object => @unlocked_users %>
      </div>
    </div>

    <div id="cnt-locked" class="dropbox">
      <h3>Locked Users</h3>
      <div id="locked_box">
        <%= render :partial => 'locked_users', :object => @locked_users %>
      </div>
    </div>
    <br clear="all" />

  </div>

lock/_unlocked_users.rhtml

the partial unlocked_users either displays a place holder element if there are no users, or calls the render of unlocked_user for each user. It also uses the AJAX function sortable_element which dictates what div container holds the sortable drag and drop elements, and what actions to take when an event is fired with them. This is how we trigger the call to the set_unlocked or set_locked action of the list controller for updating the individual "boxes" of users.


<% if unlocked_users.empty? %>
  <div class="target">  You have no Unlocked SAP Users.... </div>
<% else %>
  <%= render :partial => 'unlocked_user', :collection => unlocked_users %>
<% end %>

<%= sortable_element "unlocked_box",
  :update => "unlocked_box",
  :url => {:action=>'set_unlocked'},
  :tag => 'div', :handle => 'handle', :containment => ['unlocked_box','locked_box'] %>

<%= sortable_element "locked_box",
  :update => "locked_box",
  :url=> {:action=>'set_locked'},
  :tag => 'div', :handle => 'handle', :containment => ['locked_box','unlocked_box'] %>

lock/_unlocked_user.rhtml

the partial unlocked_user renders a dragble_element for each SapUser.


<div id="unlockeduser_<%= unlocked_user.userid %>" class="dragitem">
  <h4 class="handle"><%= unlocked_user.userid %></h4>
  <p><%= unlocked_user.last + ", " + unlocked_user.first %></p>
</div>
<%= draggable_element "unlockeduser_#{unlocked_user.userid}" %>

lock/_locked_users.rhtml

the same as for the partial unlocked_users

<% if locked_users.empty? %>
  <div class="target">  You have no Locked Users ...  </div>
<% else %>
 <%= render :partial => 'locked_user', :collection => locked_users %>
<% end %>

<%= sortable_element "unlocked_box",
  :update => "unlocked_box",
  :url => {:action=>'set_unlocked'},
  :tag => 'div', :handle => 'handle', :containment => ['unlocked_box','locked_box'] %>

<%= sortable_element "locked_box",
  :update => "locked_box",
  :url=> {:action=>'set_locked'}, 
  :tag => 'div', :handle => 'handle', :containment => ['locked_box','unlocked_box'] %>

lock/_locked_user.rhtml

the same as for the partial unlocked_user

<div id="lockeduser_<%= locked_user.userid %>" class="dragitem">
  <h4 class="handle"><%= locked_user.userid %></h4>
  <p><%= locked_user.last + ", " + locked_user.first %></p>
</div>
<%= draggable_element "lockeduser_#{locked_user.userid}" %>

In config/routes.rb add:

 map.connect '', :controller => "lock", :action => 'list'
 
and make sure that you delete public/index.html

This makes sure that any requests to the root of the server eg. http://localhost:3000, are forwarded onto the list action of the lock controller.

firing Up

Start the Rails WEBrick server by running the script:

ruby scripts/server
Open your broswer and point to http://localhost:3000.

When you connect to http://localhost:3000 (there will be an inital delay as sap4rails caches the RFC calls), you should get a screen that looks like this

A Flash movie of this in action can be seen here.

Posted by PiersHarding at 11:08 AM

September 22, 2006

saprfc and RadRails

Ruby On Rails and SAP

Last August, I wrote a weblog about SAP on Rails. In a follow up to that I'd like to explain how to import the example into an Eclipse environment called RadRails. RadRails is a specialised Eclipse install that contains a set of plugins for developing native Ruby, and Ruby on Rails applications.

The point of this blog entry is to explain how to import that example into RadRails, thus enabling all the IDE lovers out there to explore the example in their favourite environment.

All kidding aside - RadRails is a nice way to break into Rails development as it aids in visualising how a Rails application holds together.

Getting the necessary packages

The instructions for installation vary according to your OS. I will explain for both Linux, and win32.

The basic breakdown is as follows:

Install the RFCSDK

Ensure that you have the RFCSDK installed on your platform available from http://service.sap.com/connectors. For Linux this must be in /usr/sap/rfcsdk - check that librfccm.so is available in /usr/sap/rfcsdk/lib, and that it can be found at run time (you may have to add /usr/sap/rfcsdk/lib into /etc/ld.so.conf, and run ldconfig to do it).

For win32 (I've tested this on XP SP2) - make sure that the RFCSDK has been installed correctly, most likely as part of the SAP GUI client install, if you can't get it from http://service.sap.com/connectors.

Install Ruby, RubyGems, and Ruby on Rails

Detailed instructions for installing Ruby On Rails are found on the rails Website. The following is the guide for the impatient:

Install RadRails

Make sure you have a working JRE - you can get one from here - I used version 5 update 06.

Obtain and install RadRails - 0.6.2 can be downloaded from here. This is availble as a standalone IDE, and as a Plugin - what I show here is only dealing with the standalone version.

Importing the Exrates example

You should now have a workspace that looks like this:

Installing SAP::Rfc for Ruby

This part is dependent on you successfully installing the RFCSDK above.

If you are running win32 then download the saprfc 0.16 gem and install from the command line with: "gem install saprfc-0.16-mswin32.gem". Because this is a gem based install, the way that this is loaded into an application for use is different. Because of this you will need to modify SAP4Rails.rb. Navigate to this file in RadRails Exrates/lib/SAP4Rails.rb. change line 3 from: require 'SAP/Rfc'

to:

require 'rubygems'
require_gem 'saprfc'

Under Linux, get the SAP::Rfc software, unpack and follow the build instructions in the README file, which are basically:

ruby extconf.rb
make
make install

Running the Exrates example

You should now see a running server:

Trouble Shooting

If the WEBrick server does not appear to start correctly, then check the console, and the rails/Exrates/server.log, and development.log. Most times the problems are going to be with the ruby, and rails install.

As this example was developed under Linux, I have noticed that the ExratesServer defined in the bottom panel Servers tab of RadRails, needs to be recreated under windows. To do this, highlight the entry, and delete (Red X for "Remove" at top of panel). Then go File => New => Server => WEBRick Server. This should give you a dialog box with Project, Name, and Port - accept the defaults, and try starting the server again.

If there is a problem with the RFC connection, then this will appear on the console log, and further debug information should be available in the rails/Exrates/dev_rfc file.

Wrapping it up

Hopefully, you now have everything up and running - Now all I need to do is to get Craig to add this one into the "Box" :-)

Credits:

Posted by PiersHarding at 11:06 AM

September 1, 2006

sap4rails now has SAP::WAS - SAP Web Services Integraton

Following on from creating SAP::WAS for Ruby (SAP Web Services integration), sap4rails, has been enhanced to take advantage of SAP::WAS as an alternate "driver" for SAP connectivity. The latest version (0.04) can be obtained here, and an article showing an example of usage is here on SDN.

Posted by PiersHarding at 11:41 AM

New module SAP::WAS - SAP Web Services for Ruby

Announcing my new Project SAP::WAS for Ruby. This library enables SAP Web Services access encapsulated in the same manner as SAP::Rfc - abstracting away the complexities of dealing with SOAP packet encapsulation, and bringing the benefits of interface discovery.

The library can be found by following the directions here.

Posted by PiersHarding at 11:35 AM

August 12, 2006

Unicode support for Perl SAP::Rfc and Ruby saprfc

I am pleased to announce some support for UNICODE in Perl SAP::Rfc and Ruby saprfc. This is a major step forward, as it incorporates for the first time the use of the SAP supplied Unicode RFC library - librfcu*.
Please download SAP::Rfc 1.45 for Perl and saprfc-0.21 for Ruby and follow the build instructions in the README file. Make sure that you retrieve u16lit.pl form http://service.sap.com, and the appropriate rfcsdk containing the unicode libraries from SAP.
The unicode support is restricted to Client side RFC at the moment, as it is a completely separate task to replicate this for Registered RFC server programs.

Posted by PiersHarding at 7:30 PM

May 7, 2006

sap4rails - SAP RFC support for Ruby on Rails

The first version of sap4rails has been released. This has crystalised an idea of using Ruby on Rails as a Framework for developing SAP enabled web based applications.

The idea is to encapsulate SAP RFC calls from defined BAPIs so that Rails like data Models can be constructed.

Further reading can be found here.

Posted by PiersHarding at 5:07 AM

November 13, 2005

Win32 gem file available for Ruby saprfc 0.11

Olivier has done it again - Olivier is my generous win32 porter for Perl and SAP::Rfc, and now he has made SAP::Rfc for Ruby available too, as a gem package. the Ruby gem is available for download from here.
Remember - when you are using gems - instead of the 'require saprfc' to pull in the SAP::Rfc library you must use:

require 'rubygems'
require_gem "saprfc"
rfc = SAP::Rfc.new(...)
...

This opens SAP::Rfc up to all those windows users who have contacted me over the last year.

Thanks Olivier!

Posted by PiersHarding at 6:49 AM

August 4, 2005

SAP::Rfc for Ruby version 0.09

SAP::Rfc 0.09 for Ruby (SAP RFC support) has just been uploaded to RAA. This has added the return of an Array of hashes of fieldname/value pairs for table data, and a few other minor mods to support Ruby on Rails integration.

Posted by PiersHarding at 2:24 PM

August 2, 2005

SAP on Rails

Ruby on Rails is an exciting new (in an old sense) way of developing Web Services. It neatly combines convention with Best Practice, to help reduce the drag on development with the removal of declaritive cruft, allowing you to concentrate on the specifics of your application. What I want to achieve with this article, is to give you a taste for what Rails can do, and show how access to SAP business related functions can be easily integrated.

This article is also at SDN.

Ruby

Rails takes full advantage of the dynamic scripting language Ruby, which allows you to make most changes to your code without server restart, or the requirement of deployment tools. Ruby is a worthy contender in the rapid development space of web languages such as Perl and Python, with OO concepts built into the core, from the word go. With a natural intuitive style, and powerful text processing capabilities - it has all the things necessary for easy but scaleable Web Service development.

Rails Convention

Naming standards are used to reduce the amount of configuration required eg: tables relate specifically to Models, Ruby class naming conventions map directly to Controllers, Actions, and Views. Typically - the only configuration required in a Rails application is to specify the database connection parameters. Unlike some frameworks, which require extensive configuration in various incarnations of XML.

Rails Best Practice

Rails adheres to the industry design standard that is MVC, giving good separation of the data model, the application flow control, and the presentation layer composition.

On top of this, the Rails framework offers standard ways of handling errors, and error notification, templating and page componentisation, and the ability to swap these presentation formulas for different delivery channels.

Rails also has built in AJAX support available, to take advantage of the latest interest in DHTML, and flicker-free user experience.

Rails fundamentals

In order to cover the Rails fundamentals, I am going to digress here for a minute - it is important to get your head round this so that we can see where SAP integration may fit in, so please bare with me.
As was previously mentioned, Rails used the design principals of MVC (Model, View, Controller). These parts of the architecture are not configured, but are bound together through naming convention. So, if we take the classic example application of the Cookbook then the Recipe model has a module name of recipe.rb, the associated views are held in a directory ./app/views/recipe/[list|show|...].rhtml, and the controller is RecipeController in recipe_controller.rb.

Model

Classic Rails applications are centred around the ActiveRecord suite. This manages connections to a given supported database (MySQL, Postgresql, Oracle etc.). It also takes care of the auto-discovery of tables and schema, and the mapping of row/column information to OO access routines for creating, updating, deleting, and interrogating data points.
Extending the reference to the Cookbook application - the Recipe object will, by convention, be expecting a recipes table. Rails does this all automatically with some clever gramatical interogation tricks (Recipe becomes recipe, becomes recipes).
This allows a basic Model complete with CRUD operations, to be a simple incantation like this: (Note: the belongs_to: declares the association with another Model for categories - ActiveRecord manages this relationship on the fly)

Figure 1 - Recipe Model

class Recipe < ActiveRecord::Base
  belongs_to :category
end

Controller

Methods of the comtroller class (RecipeController) become the actions available to the web application. These map directly to URI names. RecipeController#list maps to /recipe/list/.... (again evidence of convention over configuration).
As you can see from the sample Controller below - each method (Action) accesses the Model to invoke the appropriate business logic to obtain the relevent data. Then the appropriate flow control is exercised (do nothing means "render with the corresponding view", where as redirect_to changes the flow to another Action).

Figure 2 - Recipe Controller

class RecipeController < ApplicationController
  layout "standard-layout"
  scaffold :recipe

  def new
    @recipe = Recipe.new
    @categories = Category.find_all
  end

  def index
    self.list
    render_action "list"
  end

  def list
    @category = @params['category']
    @recipes = Recipe.find_all
  end

  ...
end

View

The action name (for a controller) specifies which rhtml template within a view, to pick for the rendering of the output. This can also be further "skinned" by the specification of a layout (an html super set wrapper that the content is inserted into) by the "layout" directive given in the controller (see above).

Figure 3 -list() Action => list view => list.rhtml

<table border="1">
 <tr>
   <td width="40%"><p align="center"><i><b>Recipe</b></i></td>
   <td width="20%"><p align="center"><i><b>Category</b></i></td>
   <td width="20%"><p align="center"><i><b>Date</b></i></td>
 </tr>

 <% @recipes.each do |recipe| %>
   <% if (@category == nil) || (@category == recipe.category.name)%>
     <tr>
      <td>
        <%= link_to recipe.title,
                   :action => "show",
                   :id => recipe.id %>
        <font size=-1>

        <%= link_to "(delete)",
                    {:action => "delete", :id => recipe.id},
                    :confirm => "Really delete #{recipe.title}?" %>
        </font>
      </td>
      <td>
        <%= link_to recipe.category.name,
                    :action => "list",
          :category => "#{recipe.category.name}" %>
      </td>
      <td><%= recipe.date %></td>
     </tr>
   <% end %>
 <% end %>

</table>

Figure 4 - layout

<html>
 <head>
   <title>Online Cookbook</title>
 </head>
 <body>
   <h1>Online Cookbook</h1>
   <%= @content_for_layout %>
   <p>
     <%= link_to "Create new recipe",
                 :controller => "recipe",
                 :action => "new" %>
  
   <%= link_to "Show all recipes",
               :controller => "recipe",
               :action => "list" %>
     
   <%= link_to "Show all categories",
               :controller => "category",
               :action => "list" %>
   </p>
 </body>
</html>


Note: @content_for_layout determines where the body of each rendered template associated with an action is inserted.

Further reading and tutorials for Rails can be found at RubyOnRails. There are several excellent primers including:

The whole reference guide is available at API.

SAP on Rails

SAP on Rails focuses on providing an alternative type of Model template to the default - ActiveRecord.
This is done via SAP::Rfc which allows RFC calls to SAP from Ruby, with automatic handling of data types, and discovery of interface definitions for RFCs. Some of this has been covered in a previous article, and on SDN.
The model templating class is SAP4Rails. It's job is to automatically build up connections to R/3, look up the interface definitions for the collection of associated RFCs, and to hold the attribute definitions for the given model. This, coincidentally, corresponds to the SAP Business Objects view of the world in BAPIs.

Currencies and Exchange Rates

The example I have to work through is based on the two BAPI objects - Currency (BUS1090), and ExchangeRate (BUS1093). These can be found using the transaction BAPI under "Basis Services / Communication Interfaces". I have used these objects as they exist in my NW4 test drive system, so they should be available as a lowest common denominator across the board. The complete example is available at exrates.tgz. The walk through below is going to centre around the "list" action, of the exrate controller. This will show how to generate a list of exchange rates out of SAP. For this we will need to develop the Exrate Model, the ExrateController controller, and the list.rhtml view associated. Download the entire application to see all the other controller/action functions including listing, and showing details of Currencies, and the list, show, and create of Exchange rates.

Figure 5 - Currency and ExchangeRate objects in the BAPI explorer

Click to view

Each method of a business object has an associated RFC - these are what get mapped into the Rails data model.

Figure 6 - GetCurrentRates of the ExchangeRate object maps to the RFC BAPI_EXCHRATE_GETCURRENTRATES

Click to View

Start developing the Rails application

First install SAP::Rfc for Ruby. It is critical that this module is loadable in the Ruby library path - so check this first with: ruby -r "SAP/Rfc" -e 1 - this will give an error "ruby: No such file to load -- SAP/Rfc (LoadError)" if this module cannot be found. If you have installed it in an unusal way, or location, then you can try setting the environment variable RUBYLIB to the relevent directory.

Next - start the project by building the basic framework - this is done using the Rails supplied tools - execute these commands to start it off:

rails Exrates
cd Exrates
./scripts/generate controller Exrate
./scripts/generate model exrate
./scripts/generate scaffold exrate

The file layout of the application is something like this (below). Some of the files shown we have yet to create/put in place.

Figure 7 - Application layout

./config/routes.rb
./config/sap.yml                          <== RFC connection configuration
./app/controllers/exrate_controller.rb
./app/controllers/application.rb
./app/helpers/currency_helper.rb
./app/helpers/application_helper.rb
./app/helpers/exrate_helper.rb
./app/models/exrate.rb
./app/views/layouts/standard-layout.rhtml
./app/views/exrate/show.rhtml
./app/views/exrate/new.rhtml
./app/views/exrate/_form.rhtml
./app/views/exrate/list.rhtml
./public/stylesheets/scaffold.css
./lib/saprfc.so                          - SAP::Rfc files installed here
./lib/SAP/Rfc.rb                         /
./lib/SAP4Rails.rb                       <== Data Model template SAP4Rails

Note: I installed SAP::Rfc into the ./lib directory along side SAP4Rails.rb - this is just a matter of preference.

Grab SAP4Rails.rb from here and place it in the <application root>/lib directory.

Once we have this basic framework - we need to start customising the application.

SAP4Rails - the Data Model

When ./scripts/generate model exrate is executed a number of files are created, including ./app/models/exrate.rb. This contains the basic definition using ActiveRecord:

Figure 8 - Default Exrate code

class Exrate < ActiveRecord::Base
end
This needs to be changed to use SAP4Rails:

Figure 9 - Our Exrate code

require "SAP4Rails"
class Exrate < SAP4Rails
# You must define a list of RFCs to preload
  @@funcs = [
       'BAPI_EXCHANGERATE_CREATE',
       'BAPI_EXCHRATE_CREATEMULTIPLE',
       'BAPI_EXCHRATE_GETCURRENTRATES',
       'BAPI_EXCHANGERATE_GETDETAIL',
       'BAPI_EXCHANGERATE_GETFACTORS',
       'BAPI_EXCHRATE_GETLISTRATETYPES',
       'BAPI_EXCHANGERATE_SAVEREPLICA',
       'BAPI_TRANSACTION_COMMIT',
  ]

# You must define a list of attribute accessors to preload
  @@params = [ 'from',
               'extype',
               'to',
               'rate',
               'validfrom',
  ]
  
  # User defined instance methods here
  attr_accessor :message

    # user defined Class methods
  class << self
    def list
      t = Date.today
      tday = sprintf("%04d%02d%02d", t.year.to_i, t.month.to_i, t.day.to_i)
      RAILS_DEFAULT_LOGGER.warn("[Exrate] date: " + tday)
      Exrate.BAPI_EXCHRATE_GETCURRENTRATES.date.value = tday
      Exrate.BAPI_EXCHRATE_GETCURRENTRATES.call()
      return Exrate.BAPI_EXCHRATE_GETCURRENTRATES.exch_rate_list.rows()
    end
  end
end
The noticeable difference here is the two class attributes, @@funcs, and @@params. As the comments suggest - these are how we tell SAP4Rails what RFCs need to be preloaded, and what attributes our model is going to have. Technically speaking, this example does not need the @@params as this is used when building Rails update, and create features.
When the Exrate class is defined at run time, these values are processed and as the first method is accessed all of the RFC definitions, and attributes are loaded up. The full definition can be found here.
However - before SAP4Rails can interact with an SAP R/3 system, we need to tell it how to connect to one. This is done via a YAML based config file - ./config/sap.yml.

Figure 10 - RFC connection configuration

Create the file ./config/sap.yml with your connection settings, like this:

development:
  ashost: seahorse.local.net
  sysnr: "00"
  client: "010"
  user: developer
  passwd: developer
  lang: EN
  trace: "1"
Note: these values are repeated for test and production.
SAP4Rails ensures that a separate RFC connection is created for each Model - this allows a separate SAP session context per data Model, which is useful for COMMIT related issues (transactions).

The Controller

When ./scripts/generator controller Exrate is executed, amoung other things, a file ./app/controllers/exrate_controller.rb containing ExrateController, is created. The outline for this is:

Figure 11 - Default ExrateController code

class ExrateController < ApplicationController
end

We amend this with a directive for a layout to use (standard-layout), and methods index(), and list().

Figure 12 - Our ExrateController code

class ExrateController < ApplicationController
  layout "standard-layout"

  def index
    list
    render_action 'list'
  end

  def list
    @exrates = Exrate.list()
    RAILS_DEFAULT_LOGGER.warn("[LIST] exchange rates: " + @exrates.length.to_s)
  end
  ...
end
These correspond directly to the Actions index and list for the Controller exrate, which will map to a URI of /exrate/index and /exrate/list. The Action index() is just like a redirect to list() (the list() action is executed and then rendered). Within list(), we see a call upon the class method of our Model Exrate - Exrate.list(). If we refer back to the code above for class Exrate, we can see the definition for list(). It calls upon a dynamic method BAPI_EXCHRATE_GETCURRENTRATES which gives us access to an object representation of the same-named RFC.

Figure 13 - Code from Exrate#list

t = Date.today
tday = sprintf("%04d%02d%02d", t.year.to_i, t.month.to_i, t.day.to_i)
Exrate.BAPI_EXCHRATE_GETCURRENTRATES.date.value = tday
Exrate.BAPI_EXCHRATE_GETCURRENTRATES.call()
return Exrate.BAPI_EXCHRATE_GETCURRENTRATES.exch_rate_list.rows()

Looking closer at the Exrate.list() call, we see the date parameter of the RFC being set to todays date, and then the call() being made, and finally exch_rate_list.rows() is returned.

Figure 14 - Inteface definition of BAPI_EXCHRATE_GETCURRENTRATES as seen in SE37

FUNCTION bapi_exchrate_getcurrentrates.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*"  IMPORTING
*"     VALUE(DATE) LIKE  BAPI1093_2-TRANS_DATE
*"     VALUE(DATE_TYPE) LIKE  BAPI1093_2-DATE_TYPE DEFAULT 'V'
*"     VALUE(RATE_TYPE) LIKE  BAPI1093_1-RATE_TYPE DEFAULT 'M'
*"     VALUE(SHOW_PROTOCOL) LIKE  BAPI1093_2-SHOW_PROTOCOL OPTIONAL
*"  TABLES
*"      FROM_CURR_RANGE STRUCTURE  BAPI1093_3
*"      TO_CURRNCY_RANGE STRUCTURE  BAPI1093_4
*"      EXCH_RATE_LIST STRUCTURE  BAPI1093_0
*"      RETURN STRUCTURE  BAPIRET1
*"----------------------------------------------------------------------
...
The interface definition for BAPI_EXCHRATE_GETCURRENTRATES (above - via transaction SE37) shows us that exch_rate_list is a table parameter, and the rows() method is returning an Array of table lines. SAP::Rfc takes care of all of this, including carving up the rows based on the table structure definition, making each line a hash of fieldname/value pairs.

Going back to the Controller - at this level, SAP RFC specificness has been abstracted away - the only hint is directly calling error methods (see the Exrate#save method definitition) - this is usually more concealed in ActiveRecord, because of the Database Schema related information being stored in the Model - this is a lot harder to achieve in relation to RFC calls, as the schema relating to an RFC is less definitive, than that of classic Rails database table design - see the Todo list, and Cookbook tutorials to get further details. SAP4Rails uses the ActiveRecord::Errors class which is bound to an inherited attribute of the Model called @errors. When the facilities of this class are used then error testing, recording, and then subsequent display is trivial with the inclusion of the error_messages_for directive in a view template (see fragment _form.rhtml via new.rhtml for exrate, and observe how the Exrate#save interacts with it).

Other than that - from here on in it is just plain Rails.

The Views

Now on to rendering the output. The embedded Ruby code of the list.rhtml template gets executed in the context of the instantiated ExrateController, so it has access to all the attributes of that object. In the interests of tidiness, I have put the column/field names relating to list() Array results in a Helper module ./app/helpers/exrate_helper.rb. This is inherited by the controller at run time, so anything defined here is also available in the template.

Figure 15 - ExrateController helper module ExrateHelper

module ExrateHelper
  # Fields for form output
  def formFields
    return    [ 'FROM_CURR',
                'TO_CURRNCY',
                'RATE_TYPE',
                'TO_FACTOR',
                'EXCH_RATE',
    ]
  end
end
There is so much going on within the template, but specific to our SAP4Rails example, we can see the code iterating over the attribute Array of @exrates - each element of which, holds a hash of field/value pairs. This is where the field names from formFields() are also iterated over to print out the columns.

Figure 16 - exrate template list.rhtml

<h1>Listing Exchange Rates</h1>
<%= link_to 'Currencies', :controller => 'currency', :action => 'list' %>
  
<table>
  <tr>
  <!-- get fields from ExrateHelper -->
  <% formFields.each {|key| %>
    <th><%= key %></th>
  <% } %>
  </tr>
  
  <% for exrate in @exrates %>
  <tr>
    <% formFields.each {|val| %>
    <td>
      <% if val == "FROM_CURR" or val == "TO_CURRNCY" %>
        <%= link_to exrate[val], :controller => 'currency', :action => 'show',
                                    :id => exrate[val].strip %>
      <% else %>
        <%=h exrate[val] %>
      <% end %>
      </td>
    <% } %>
    <td>
    <%= link_to 'Show', :action => 'show', :id => exrate['RATE_TYPE'].strip + ':'
                   + exrate['FROM_CURR'].strip + ':' +
                     exrate['TO_CURRNCY'].strip %>
    <%= link_to 'Edit', :action => 'new', :id => exrate['RATE_TYPE'].strip + ':'
                   + exrate['FROM_CURR'].strip + ':' + 
                     exrate['TO_CURRNCY'].strip %>
    </td>
  </tr>
  <% end %>
</table>

And with that - here are the results.

Figure 17 - Listing Exchange Rates - rendered list.rhtml

In conclusion

The approach laid out in this article, has enabled a high level of integration with the Rails framework. The amount of code required to make this possible (SAP4Rails.rb) is less than 130 lines, which I think is testimony to how flexible, and well designed Rails is. Further more - in the data Models shown, the heavy lifting of wrestling with RFC parameters, and their associated data types has been made trivial with SAP::Rfc and it's auto-interface-discovery features. Have a closer look at the resources included (see above) - and you can see why Rails is creating such a storm in the world of Open Source, and making waves in the "Traditional Commercial Web development" Arena.

Now you can get on track with your Web Services development. :-)

Posted by PiersHarding at 7:48 PM | Comments (4)

July 6, 2005

SAP::Rfc and SAP::Rfc gives SAP RFC

SAP::Rfc 1.37 for Perl, and SAP::Rfc 0.08 for Ruby have just been uploaded to their respective repositories. These are bug fixes for BCD value handling where a BCD (Binary Coded Decimal) number has 0 decimal places.

Posted by PiersHarding at 9:32 AM

June 16, 2005

Ruby on Rails and ActiveRecord

Ruby on Rails is a celebrated recent phenomenon, and like many others, I have been seduced by its ease, and simplicity. One of the things that I have found with its MVC style approach, is that you really need to have a good grip on the design of your data requirements. At the heart of this is ActiveRecord which is the Model.

Inorder to understand ActiveRecord more, I played around with using it outside of Rails ( I know that you can use the console ), to see what you can do. Seeing that I had a hard time finding much information about this (more specifically - examples), I have written up a small example that I worked through to demonstrate the way ActiveRecord encapsulates a "many to many" relationship.

I started with two entities - Users and Roles, and wished to describe:

Firstly - define the tables - ActiveRecord takes the approach of defining a table per entitiy, and then a separate table to hold relationship between the entities, and as with all things Rails, it relies heavily on naming convention to knit it together.
Users => users
Roles => roles
Roles <=> User => roles_users

CREATE TABLE roles (
  id int(10) unsigned NOT NULL auto_increment,
  name varchar(50) NOT NULL default '',
  info varchar(50) default NULL,
  PRIMARY KEY  (id)
) TYPE=MyISAM;

INSERT INTO roles VALUES (1,'role2','some role 2 info');
INSERT INTO roles VALUES (2,'role1','some role 1 info');

CREATE TABLE roles_users (
  role_id int(10) unsigned NOT NULL default '0',
  user_id int(10) unsigned NOT NULL default '0',
  KEY ur_map_idx (role_id,user_id)
) TYPE=MyISAM;

INSERT INTO roles_users VALUES (1,1);
INSERT INTO roles_users VALUES (2,1);

CREATE TABLE users (
  id int(10) unsigned NOT NULL auto_increment,
  nick varchar(50) NOT NULL default '',
  name varchar(50) default NULL,
  password varchar(50) NOT NULL default '',
  modified timestamp(14) NOT NULL,
  created timestamp(14) NOT NULL,
  access timestamp(14) NOT NULL,
  PRIMARY KEY  (id)
) TYPE=MyISAM;

INSERT INTO users VALUES (1,'pxh','Piers Harding','blah',20050512121552,20050512121552,20050512121552);

Next thing is to create the Model, and the code to access it. As this is installed using Ruby Gems, you must import gems, and then use gems to bootstrap the loading of ActiveRecord.

#!/usr/bin/ruby
# load gems, and then ActiveRecord
require 'rubygems'
require_gem 'activerecord'

# Establish a conneciton to your database
  ActiveRecord::Base.establish_connection(
      :adapter  => "mysql",
      :host     => "badger",
      :username => "root",
      :password => "",
      :database => "auths"
   )

# Define the entity Role
class Role < ActiveRecord::Base
# describe the relationship to users
  has_and_belongs_to_many :users
end

# same for users
class User < ActiveRecord::Base
  has_and_belongs_to_many :roles
end

# get the first user
user = User.find(1)

# find the roles for a user
roles = user.roles

# loop through each role
roles.each {|role|
             print "Role: #{role.info}\n"
# find users for the role
             users = role.users
             users.each {|u|
# and again find the roles for each user
                userroles = u.roles
                print "Roles for user #{u.name} \n"
                userroles.each{|r| print "\t\t -->role: #{r.name}\n"}
                }
          }

Hope this helps someone.

Posted by PiersHarding at 12:33 PM