Batch API - API chaining scenario


In this example, we have two back­end services and one proxy service. WSDLs for the
examples can be found in the  zip file attached. This service is in the WSO2 API manager and it is exposed to customers. This service accepts
an ID and a credit amount for its credit operation. A request coming to this service is served by
two back­end services.
PersonInfoService
The PersonInfoService provides the name and address information about a requestor when the
ID is given. So, this is the first service being called by the CreditProxy service deployed in the API
The CreditService is the actual service that does the crediting. It is called by the CreditProxy
after getting the required information from the PersonInfoService.
The zip file attached which contains all the necessary configurations for the scenario. The
scenario needs WSO2 API Manager and WSO2 Application Server. The files in the zip should
be copied or deployed into these servers. Now, let's look at the contents of the zip file and
how to use them to create the scenario.
Please use this[1] link to download all resources for this example.
[1]https://drive.google.com/file/d/0B3OmQJfm2Ft8ZWRxRlUxd1VTeXM/edit?usp=sharing

Back-End services
Deploy the esb-samples-1.0-SNAPSHOT.jar as a jar service in to the WSO2 App Server. This
jar has two POJO services. If you need to try this with your backend services you can try
that as well. The required classes that need to be exposed as web services are
● org.wso2.esb.samples.CreditService
● org.wso2.esb.samples.PersonInfoService

API Manager Configuration
1. Copy the contents of following folders in the sample zip to the
repository/deployment/server/synapse-configs/default/ of WSO2 API manager pack.
            /local­entries/xslt.xml
/proxy­services/CreditProxy.xml
            /endpoints/CreditEpr.xml
            /endpoints/PersonInfoEpr.xml
/sequences/creditSeq.xml
/sequences/personInfoSeq.xml
Here i have attached API configs as well. But i recommend you to create 2 APIs for
services using API publisher UI. Both of them should be pointed to actual services hosted in
WSO2 Application Server.
2. Copy the personToCredit.xslt in the sample zip to wso2am-1.6.0/resources/
directory of WSO2 API Manager.
3. Copy the CreditProxy.wsdl in the sample zip to the wso2am-1.6.0/resources/
directory of the WSO2 API Manager.
4. If you are running the WSO2 API Manager in the sample machine as the WSO2
Application Server, change the ports of the WSO2 API Manager in
respository/conf/transport-mgt.xml. Otherwise there will be port conflicts.
Now we have the complete configuration for the scenario, let's go through the API Manager
configuration step by step to understand how it works.
Here we have one main entry point(credit Proxy or credit API).
● Receive a request with only credit amount and ID of the requestor.
● Send the id to the PersonInfoService to get the Address and Name of the requestor
● Use the credit amount, ID, address and name to create a request to the credit service
and call the CreditService
The request to the proxy service is:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:sam="http://samples.esb.wso2.org">
    <soapenv:Body>
        <sam:credit>
            <sam:id>99990000</sam:id>
            <sam:amount>1000</sam:amount>
        </sam:credit>
    </soapenv:Body>
</soapenv:Envelope>
The information in the original request is required to call the two back­end services. When we do
the first request, the information from the original request will be lost if we don’t preserve it. So,
we need to store the required information from the original request in the message context. In
this case, we are only going to store the ID and the credit amount in the message context. But, it
is possible to store any part of the message in the context. For example, in some scenarios, the
whole message is stored.
We store the request ID and amount by extracting them using property mediator with XPath.
<property xmlns:sam="http://samples.esb.wso2.org" name="ORG_ID"
expression="//sam:credit/sam:id"/>
<property xmlns:sam="http://samples.esb.wso2.org" name="ORG_AMOUNT"
expression="//sam:credit/sam:amount"/>
After this step, the ID and the credit amount are available in the Message Context properties
named ORG_ID and ORG_AMOUNT respectively.
Now, we are ready to do the first request to the PersonInfoService. We need the XML for this
request and, so, will insert it into the message using the Enrich Mediator.
    <source type="inline" clone="true">
        <sam:get xmlns:sam="http://samples.esb.wso2.org">
        <sam:id>?</sam:id>
        </sam:get>
    <target type="body"/>
Then we will log message details and send request to PersonInfoEpr end point. Also please note
that we have set receive sequence here. Then response will directly goto mentioned
sequence(personInfoSeq). See following configuration.
<log level="full">
                    <property name="sequence" value="inSequence ­ request for PersonInfoService"/>
Send request to PersonInfoEpr
                <send receive="personInfoSeq">
                    <endpoint key="PersonInfoEpr"/>
                </send>
Here you will see personInfoSeq configuration. There we have used xslt transformation to
convert message to desired format for credit service. There also you will noticed that response
will direct to creditSeq. If you need to do some change for final response message you are free
    <sequence name="personInfoSeq">
        <xslt key="xslt">
    <property name="amount" expression="get­property('ORG_AMOUNT')"/>
        <property name="Action" value="urn:credit"/>
        <send receive="creditSeq">
            <endpoint key="CreditEpr"/>
Here is the creditSeq configuration. There we log message and send response to client directly.
    <sequence name="creditSeq">
        <log level="full"/>
So this will cover complete service chaining scenario using batch API. According to your specific
scenarios you might have to add more complex logics. I have tested this scenario with WSO2
API Manager 1.5.0 and Application Server 5.2.1.
When you invoke this proxy service create 2 APIs to credit service and personal information
service. Then subscribe both of them to single Application.
Then generate tokens for that application and invoke proxy service with that(for this example i
used proxy service if you need create API for that).
Inside proxy service we will extract auth headers and store it inside message context then we
use that token to each and every following API calls. See following configuration.
<property name="Auth" expression="get­property('transport','Authorization')"/>
We used above to extract auth header and store it for following API calls. This is how we inject
auth headers for each API call.
<property name="Authorization" expression="get­property('Auth')" scope="transport"
type="STRING"/>
If you enabled API manager wire logs you will see following complete logs for each incoming and
outgoing messages. When you try this sample always set your API  urls to eprs and use them
inside your configuration. Then once you changed API with newer version or url do don't have to
change all the places.
[2014­02­05 15:38:45,775]  INFO ­ SequenceDeployer Sequence: main has been updated from the file:
/home/sanjeewa/work/workflow/wso2am­1.6.0­1/repository/deployment/server/synapse­configs/default/sequences/main.xml
[2014­02­05 15:38:46,684] DEBUG ­ wire >> "POST /services/CreditProxy.CreditProxyHttpSoap12Endpoint HTTP/1.1[\r][\n]"
[2014­02­05 15:38:46,684] DEBUG ­ wire >> "Content­Type: application/soap+xml; charset=UTF­8; action="urn:credit"[\r][\n]"
[2014­02­05 15:38:46,684] DEBUG ­ wire >> "Cookie: menuPanel=visible; menuPanelType=main;
region2_humantask_menu=visible; region2_bpel_instances_menu=visible; region3_registry_menu=none; i18next=en­US;
JSESSIONID=1579A304FBF0FE9AA2BEE6B9A7C9F85A;
requestedURI="../../carbon/service­mgt/index.jsp?region=region1&item=services_list_menu";
current­breadcrumb=manage_menu%2Cservices_menu%2Cservices_list_menu%23;
MSG13915816906030.6012256733392928=true; region1_configure_menu=none; region4_monitor_menu=none;
region5_tools_menu=none[\r][\n]"
[2014­02­05 15:38:46,684] DEBUG ­ wire >> "User­Agent: Axis2[\r][\n]"
[2014­02­05 15:38:46,684] DEBUG ­ wire >> "Host: sanjeewa­ThinkPad­T530:8280[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "130[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><p:credit
xmlns:p="http://samples.esb.wso2.org"><!­­0 to 1 occurrence­­><p:id>33</p:id><!­­0 to 1
occurrence­­><p:amount>2</p:amount></p:credit></soapenv:Body></soapenv:Envelope>[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "0[\r][\n]"
[2014­02­05 15:38:46,685] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,688]  INFO ­ LogMediator To: /services/CreditProxy.CreditProxyHttpSoap12Endpoint, WSAction: urn:credit,
SOAPAction: urn:credit, MessageID: urn:uuid:4f418b82­d739­4cc8­bce1­f1fbd54659b3, Direction: request, sequence =
inSequence ­ request for CreditProxy, Envelope: <?xml version="1.0" encoding="utf­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><p:credit
xmlns:p="http://samples.esb.wso2.org"><!­­0 to 1 occurrence­­><p:id>33</p:id><!­­0 to 1
occurrence­­><p:amount>2</p:amount></p:credit></soapenv:Body></soapenv:Envelope>
[2014­02­05 15:38:46,690]  INFO ­ LogMediator To: /services/CreditProxy.CreditProxyHttpSoap12Endpoint, WSAction: urn:credit,
SOAPAction: urn:credit, MessageID: urn:uuid:4f418b82­d739­4cc8­bce1­f1fbd54659b3, Direction: request, sequence =
inSequence ­ request for PersonInfoService, Envelope: <?xml version="1.0" encoding="utf­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><sam:get
xmlns:sam="http://samples.esb.wso2.org">
                            <sam:id>33</sam:id>
                        </sam:get></soapenv:Body></soapenv:Envelope>
[2014­02­05 15:38:46,691] DEBUG ­ wire << "POST /services/PersonInfoService/ HTTP/1.1[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "Cookie: menuPanel=visible; menuPanelType=main;
region2_humantask_menu=visible; region2_bpel_instances_menu=visible; region3_registry_menu=none; i18next=en­US;
JSESSIONID=1579A304FBF0FE9AA2BEE6B9A7C9F85A;
requestedURI="../../carbon/service­mgt/index.jsp?region=region1&item=services_list_menu";
current­breadcrumb=manage_menu%2Cservices_menu%2Cservices_list_menu%23;
MSG13915816906030.6012256733392928=true; region1_configure_menu=none; region4_monitor_menu=none;
region5_tools_menu=none[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "Content­Type: application/soap+xml; charset=UTF­8; action="urn:credit"[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "Host: localhost:9764[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "Connection: Keep­Alive[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "User­Agent: Synapse­PT­HttpComponents­NIO[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "124[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><sam:get
xmlns:sam="http://samples.esb.wso2.org">[\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "                            <sam:id>33</sam:id>[\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "                        </sam:get></soapenv:Body></soapenv:Envelope>[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "0[\r][\n]"
[2014­02­05 15:38:46,692] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "HTTP/1.1 200 OK[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "Content­Type: application/soap+xml;charset=UTF­8[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "Date: Wed, 05 Feb 2014 10:08:46 GMT[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "Server: WSO2 Carbon Server[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,695] DEBUG ­ wire >> "200[\r][\n]"
[2014­02­05 15:38:46,696] DEBUG ­ wire >> "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><ns:getResponse
xmlns:ns="http://samples.esb.wso2.org"><ns:return xmlns:ax2461="http://samples.esb.wso2.org/xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema­instance" xsi:type="ax2461:PersonInfo"><ax2461:address>59, Flower Road,
Lanka</ax2461:address><ax2461:id>33</ax2461:id><ax2461:name>WSO2</ax2461:name></ns:return></ns:getResponse></soa
penv:Body></soapenv:Envelope>[\r][\n]"
[2014­02­05 15:38:46,697] DEBUG ­ wire >> "0[\r][\n]"
[2014­02­05 15:38:46,697] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,706] DEBUG ­ wire << "POST /services/CreditService/ HTTP/1.1[\r][\n]"
[2014­02­05 15:38:46,706] DEBUG ­ wire << "Content­Type: application/soap+xml;charset=UTF­8[\r][\n]"
[2014­02­05 15:38:46,707] DEBUG ­ wire << "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,707] DEBUG ­ wire << "Host: localhost:9764[\r][\n]"
[2014­02­05 15:38:46,707] DEBUG ­ wire << "Connection: Keep­Alive[\r][\n]"
[2014­02­05 15:38:46,707] DEBUG ­ wire << "User­Agent: Synapse­PT­HttpComponents­NIO[\r][\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "200[\r][\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><sam:credit
xmlns:sam="http://samples.esb.wso2.org" xmlns:xsd="http://samples.esb.wso2.org/xsd"
xmlns:ax21="http://samples.esb.wso2.org/xsd">[\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "<sam:info>[\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "<xsd:amount>2</xsd:amount>[\n]"
[2014­02­05 15:38:46,708] DEBUG ­ wire << "<xsd:personInfo>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "<xsd:address>59, Flower Road, Colombo 07, Sri Lanka</xsd:address>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "<xsd:id>33</xsd:id>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "<xsd:name>WSO2</xsd:name>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "</xsd:personInfo>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "</sam:info>[\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "</sam:credit></soapenv:Body></soapenv:Envelope>[\r][\n]"
[2014­02­05 15:38:46,709] DEBUG ­ wire << "0[\r][\n]"
[2014­02­05 15:38:46,710] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "HTTP/1.1 200 OK[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "Content­Type: application/soap+xml;charset=UTF­8[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "Date: Wed, 05 Feb 2014 10:08:46 GMT[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "Server: WSO2 Carbon Server[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,712] DEBUG ­ wire >> "109[\r][\n]"
[2014­02­05 15:38:46,713] DEBUG ­ wire >> "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><ns:creditResponse
xmlns:ns="http://samples.esb.wso2.org"><ns:return>true</ns:return></ns:creditResponse></soapenv:Body></soapenv:Envelope
[2014­02­05 15:38:46,713] DEBUG ­ wire >> "0[\r][\n]"
[2014­02­05 15:38:46,713] DEBUG ­ wire >> "[\r][\n]"
[2014­02­05 15:38:46,714]  INFO ­ LogMediator To: http://www.w3.org/2005/08/addressing/anonymous, WSAction: ,
SOAPAction: , MessageID: urn:uuid:10f13946­23dc­49e0­97ab­a369c2101a9e, Direction: response, Envelope: <?xml
version="1.0" encoding="utf­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><ns:creditResponse
xmlns:ns="http://samples.esb.wso2.org"><ns:return>true</ns:return></ns:creditResponse></soapenv:Body></soapenv:Envelope
[2014­02­05 15:38:46,717] DEBUG ­ wire << "HTTP/1.1 200 OK[\r][\n]"
[2014­02­05 15:38:46,717] DEBUG ­ wire << "Content­Type: application/soap+xml;charset=UTF­8[\r][\n]"
[2014­02­05 15:38:46,717] DEBUG ­ wire << "Date: Wed, 05 Feb 2014 10:08:46 GMT[\r][\n]"
[2014­02­05 15:38:46,717] DEBUG ­ wire << "Server: WSO2­PassThrough­HTTP[\r][\n]"
[2014­02­05 15:38:46,717] DEBUG ­ wire << "Transfer­Encoding: chunked[\r][\n]"
[2014­02­05 15:38:46,717] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:46,718] DEBUG ­ wire << "109[\r][\n]"
[2014­02­05 15:38:46,718] DEBUG ­ wire << "<?xml version="1.0" encoding="UTF­8"?><soapenv:Envelope
xmlns:soapenv="http://www.w3.org/2003/05/soap­envelope"><soapenv:Body><ns:creditResponse
xmlns:ns="http://samples.esb.wso2.org"><ns:return>true</ns:return></ns:creditResponse></soapenv:Body></soapenv:Envelope
[2014­02­05 15:38:46,718] DEBUG ­ wire << "0[\r][\n]"
[2014­02­05 15:38:46,718] DEBUG ­ wire << "[\r][\n]"
[2014­02­05 15:38:47,786]  INFO ­ SequenceDeployer Sequence: fault has been updated from the file:
/home/sanjeewa/work/workflow/wso2am­1.6.0­1/repository/deployment/server/synapse­configs/default/sequences/fault.xml

How to avoid getting empty response to client due to slowness in key validation call - WSO2 API Manager

Users may get empty response due to slowness in key validation call due to the possibility of timeout gateway to key manager service call. These timeouts can happen in many different ways like below.
01. Connection timeout happen during establishment phase.
We can address this using axis2 client configuration change. Default connection timeout is 60 seconds and we can change that using below property.
Connection Timeout - the time to establish a connection with the remote host
"CONNECTION_TIMEOUT"
>30000
02. Socket timeout due to inactivity to wait packets to arrive.
We can configure this value as well through axis2 client configuration change. Default value of this property is 60 seconds and we can change that as required.
Socket Timeout - this is the time of inactivity to wait for packets to arrive
"SO_TIMEOUT"
>30000
03. Delay due to slow healthy connection .
In this case connection establishment and packet sending happens as usual. But data transfer between server and client getting delayed. In this case what happens is http client keep sending data to server till it accepts them.  We can recreate this by adding slow proxy or something like that. Here important thing is if we notice this type of delay then all UI operations including logging, token retrieval, update etc will also effect. We do not have specific property to override this waiting time from http client level. After 2 minutes this throws error. But before that after 1 minute source handler getting timeout and user will get empty response as it breaks connection.
To send some sort of error to client source handler should wait more than 2 minutes without getting timeout. So if we increase passthrough level socket timeout to 3 mins or so then it will wait till key manager error comes due to data send error. Then key validation handler will send proper unclassified authentication error with error code. Increasing passthrough socket timeout will effect all APIs deployed in the system as its transport level property. But most of the cases proper clients will have timeout values in client level so they will not effect due to this changes. If some client waits forever without having timeout then they will get error like below.
{"fault":{"code":900900,"message":"Unclassified Authentication Failure","description":"Error while accessing backend services for API key validation"}}
To set socket timeout please edit following property in passthru-http.properties.
http.socket.timeout=180000
Please refer this[1] document to understand more about client properties.
[1]http://hc.apache.org/httpclient-3.x/preference-api.html#HTTP_connection_parameters

How to manage external/internal gateways and store with single publisher - WSO2 API Manager

We usually use external API stores only for advertising purposes. If they need to consume API then they have to come back to original store and subscribe there. But as we see your requirement is slightly different from that.

Let me explain you how most of our users handle API usability for internal/external users. I'm sure these external and internal users are resides in different user groups. If that is the case, then we will be able to map them to different user roles. And then we can set the visibility to only required group. For an example if we have API which expose sensitive information then we can set visibility of that API to internal_role role. Then users with internal_role role can only see that API, External users will not be able to see that API or subscribe that.

If we think of deployment then, API Store node will be deployed inside corporate network and outside network pointing to same database. Based on logged in user we can show them right APIs. This solution proposed to you assuming same data shared between both external and internal deployments.

Let me give you exact steps we need to follow,
01. Create 2 different roles for internal and external users(named internal and external).
02. Deploy API Manager internal and external gateways and configure publisher to publish them selectively.
03. Store also can deploy internally and externally pointing to same databases.
04. To create internal only API, create API with limited viability to internal role. Then publish this API to internal gateway.
When internal user logged into internal API store they will see API and can consume it(as they have internal role).
When external user logged into external API store they will not see above API and cannot use that. If they try to access API from external gateway then it will also failed as API not deployed there.
05. To create internal/external API, create API with limited viability to both internal and external role. Then publish this API to both internal and external gateways.
When external user logged into external API store they will see API and can consume it(as they have external role).
When internal user logged into internal API store they will see above API and can use that. If they try to access API from external gateway then it will also work as API deployed there.
06. To create external only API, create API with limited viability to external role. Then publish this API to external gateway.
When external user logged into external API store they will see API and can consume it(as they have external role).
When internal user logged into internal API store they will not see above API and cannot use that. If they try to access API from internal gateway then it will also failed as API not deployed there.

Empowering the Future of API Management: Unveiling the Journey of WSO2 API Platform for Kubernetes (APK) Project and the Anticipated Alpha Release

  Introduction In the ever-evolving realm of API management, our journey embarked on the APK project eight months ago, and now, with great a...