Announcement: You can find the guides for Commerce 7.5 and later on the new Elastic Path Documentation site. The Developer Center continues to support Commerce 6.13.0 through 7.4.1.Visit new site

Tutorial 5 - Creating a Resource Integration

Tutorial 5 - Creating a Resource Integration

This class explains how to create aresource integration and swap it with another resource integration.

Suppose you are tasked with creating a new assets resource integration to communicate with your company's backend system—a backend system that is not the Elastic Path Commerce Engine. You'll want to do this without having to create a resource integration from scratch, perform extensive customizations in Cortex API, read tons of technical documents, and so on. This lesson takes you through, step by step, how to quickly create an assets resource integration project, code it, and then swap it with the out-of-the-box assets integration resource.

What you should already know:

Warning: Before you begin

Make sure your development environment has been properly set up as described in Setting up your Developer Environment.

Note: Tutorial 5 Installation

Tutorial 5 comes pre-configured to run in the Tutorial Web Application. The instructions below describe how to add this project to your Cortex API web application that you created in Generate Cortex Projects.

Generate a New Resource Integration Project

Creating a new Resource Integration project is the first lesson in this class. Elastic Path provides a Maven Archetype to simplify the task to create a new resource integration. Archetypes are project templates that contain some of the standard components you'll need for your project.

  1. Open a command prompt and navigate to your Extensions Directory.
  2. Run the following command to create a Cortex API Resource Integration project:
    mvn archetype:generate -DarchetypeArtifactId=ep-cortex-resource-integration-archetype -DarchetypeGroupId=com.elasticpath.cortex.dce -DarchetypeVersion=<your-artifact-version>
    Tip: Stack Trace Tip

    You may see a stack trace preceded by a warning similar to the following:

    [WARNING] Error reading archetype catalog http://repo1.maven.org/maven2
    org.apache.maven.wagon.TransferFailedException: Error transferring file: Connection timed out: connect
                

    This can occur if you are operating Maven in online mode. It can be safely ignored.

    Tip: Troubleshooting Tip

    Build fails when generating the archetype? See: Build Troubleshooting.

  3. Enter the following information when prompted:
    Table 2. Archetype Variable Values
    Variable Value
    groupId com.elasticpath.tutorials
    artifactId example-assets-resource-integration
    version 1.0-SNAPSHOT
    package com.elasticpath.tutorials
  4. Change to the newly created project directory and execute the following command to build the resource integration JAR file:
    mvn install

Import the New Resource Integration into Eclipse

Next, you need to import your Resource Integration project into Eclipse as a Maven project.

  1. In Eclipse, choose File -> Import -> Existing Maven Projects into Workspace, and click Next.
  2. Click Browse and select the resource integration directory.
  3. Make sure the project is selected in the list of projects and click Finish.
  4. Once the Maven project imports, it should look like this project.png

Add the Resource Integration's Dependency

Once our base project is set up, we need to add the appropriate rest resource dependency to it. Because we are building an assets resource integration, we'll add the assets rest resource dependency. If you were building a resource integration for another rest resource, you would add a different dependency. For example, if you were creating a carts resource integration, you would add the carts rest resource as a dependency. We recommend having one rest integration per rest resource, as this will simplify development and keep in line with Java's single responsibility principle.

Why do you need this dependency?

To create our assets resource integration, we need to implement the assets rest resource's lookup strategy interface. Therefore, we need to add the assets resource as a dependency to our project so we can implement the interface.

To add the dependency:

  1. In Eclipse, open your example-assets-resource-integration project's pom.xml.
  2. Add the following code beneath the <description> element:
    <description>Elastic Path REST - ResourceName Resource Commerce Engine Integration</description>
    ...
    <dependencies>
    	    <dependency>
    	        <groupId>com.elasticpath.rest.resource</groupId>
    			<artifactId>ep-resource-assets</artifactId>
    			<version>0.1.0-SNAPSHOT</version>
    	    </dependency>
    	</dependencies>
  3. Save the pom.xml.
  4. Right-click the example-assets-resource-integration project and select Maven -> Update Project Configuration, to update your project's dependencies and rebuild.

Tip: Dependencies List

For a list of rest resource dependencies, see your cortext-dce\webapp-parent\pom.xml.

Implement the Lookup Strategy Interface

Now that we've created the project and added the appropriate dependency, we're going to implement the assets' lookup strategy interface. But first, let's get an overview of the Integration Layer so we can better understand how and why we are coding this interface.

How does the Integration Layer work?

Your backend system could be anything, Elastic Path's Commerce Engine, Aria's Subscription Billing Service, and so on. The Integration Layer, the layer where you are building your resource integration, is designed so you can customize it to talk to any backend system. Take a look at steps number6.png and number7.png in Architecture Call Stack and notice how the resource integration returns a DTO to the rest resource. As long as the resource integration returns the proper DTO for the rest resource to consume, the resource integration could be communicating with any backend system to construct that DTO, not just the Elastic Path Commerce Engine.

What do I need to code?

Rest resources have lookup and writer strategy interfaces that define the data they need to complete an operation. What you are coding in your resource integration is the rest resource's interface. For this tutorial, the code you need to implement is shown in the pseudo UML diagram to the right. As you can see, you'll create a class calledAssetLookupStrategyImplthat implements the AssetLookupStrategy. The AssetsLookupStrategy interface has two methods that your AssetLookupStrategyImpl class will implement. We will explain these methods later in this lesson.

If you are coding something other than an assets resource integration, you'll need to implement different interface methods. These are not covered in this tutorial. To code these methods, take a look at the Rest Resources Java Docs, particularly the rest resource's interface javadoc, to get an understanding of what you'll need to code.

assetDTO

Create the LookupStrategyImpl Class

Create the look up strategy's implementation class. In this tutorial, we are creating an assets resource integration, so you'll need to create a class that implements the assets rest resource interface. If you were creating a resource integration for another rest resource, you would create a class that implements the lookup and writer strategies for that resource. For example, if you were coding a lookup implementation for the cart resource, you would implement the CartLookupStrategy and code it's methods: getCart() and isCartOwnedByUser().

To Create the Asset LookupStrategyImpl Class

  • In your Eclipse project, create a new class with the following details:
    • Source Folder: example-assets-resource-integration/src/main/java
    • Package: com.elasticpath.tutorials
    • Name: AssetLookupStrategyImpl
    • Interface: com.elasticpath.rest.resource.assets.integration.AssetLookupStrategy newJavaClass.png

Code the Strategy Implementation

The rest resource's lookup and writer strategy interfaces define what you'll need to code in your resource integration. Our tutorial uses the assets rest resource as an example, which has a Lookup strategy with two methods. Below, we explain how to code these two methods; however, these are just examples. We don't actually create a backend system to talk to, we hard-code the asset's details in our examples. If you are coding a different resource integration, you need to implement different methods. Use the descriptions below as a guide for how to code the methods in your resource integration.

Name your Implementation Bean

In the Cortex API, we use Javax.Inject to name our Java Beans, so you'll need to declare your bean's name above your class declaration:

@Named("assetLookupStrategy")
...
public class AssetLookupStrategyImpl implements AssetLookupStrategy {

The first method we'll implement is public ExecutionResult<AssetDto> getAsset(String scope, String decodedAssetId). This method's purpose is to find the asset identified in the decodedAssetId parameter and return it as an AssetDto. On Cortex API client application side, this method satisfies the GET - an asset HTTP request.

Method Parameters:

  • String decodedItemId - Contains the assets's ID This could be a UID, GUID, or something else your backend system understands. Notice the method uses the asset's decoded ID Once the ID goes up the Cortex API stack, it gets encoded so client applications don't see real I.Ds. For more on this, see ID Encoding.
  • String scope - Identifies the store's domain (not used in this tutorial).

The following snippet creates an AssetDto, defines the DTO's values, sets the operation' status, and returns an ExecutionResult to the assets rest resource.

@Override
	public ExecutionResult<AssetDto> getAsset(String scope,
			String decodedAssetId) {

		AssetDto assetDto = ResourceTypeFactory.createResourceEntity(AssetDto.class);		
		
		assetDto.setContentLocation("http://mobee.elasticpath.com/avatar.jpg");
		assetDto.setDisplayName("Avatar");
		assetDto.setName("A12345");
		assetDto.setRelativePath("avatar.jpg");
		
		ExecutionResult<AssetDto> result = ExecutionResultFactory.createReadOK(assetDto);
		
		return result;
	}

Notice how the code flows here (most lookup strategy implementations follow this structure):

1. Create a DTO

We use the ResourceTypeFactory to instantiate the AssetDTO. This factory design pattern makes it easier to transform the DTO into a representation later on when the request percolates up the Cortex API stack. You don't need to worry about the representation for what you are coding here in the integration layer. Just be aware that your DTO eventually gets converted into a representation, which then surfaces back to the client application.

AssetDto assetDto = ResourceTypeFactory.createResourceEntity(AssetDto.class);
2. Get the asset data

We don't actually get the asset data from a backend system in this sample, we hard code it here. However, if you were coding your integration resource to talk to your backend system, you would use the decodedAssetId to retrieve the asset from your backend system and then convert whatever is returned by your backend system into an assetDto. The tutorial's source code, which you can download from the table of contents, uses a service class to retrieve the asset details from a CSV file. Keep this in mind when you are coding your resource integration because you may need to use a service like when communicating with your backend system.

3. Set the Operation's Status

Set the operation's status using the ExecutionResultFactory to indicate the operation succeeded or failed. In our tutorial, we set it to ExecutionResultFactory.createReadOK(). If we couldn't find the asset, we would set the status to ExecutionResultFactory.createNotFound(). See the class for the available statuses.

ExecutionResult<AssetDto> result = ExecutionResultFactory.createReadOK(assetDto);

4. Return the Result

Lastly, return the AssetDto in an ExecutionResult. We wrap the assetDto in an ExecutionResult because we do not throw exceptions in the Cortex API. Instead of throwing an exception, we return an ExecutionResult with a ResourceStatus to indicate the operation's success or failure. Indicating the operation's status this way, instead of throwing an exception with a convoluted stack trace, allows the Cortex API to fail gracefully.

The second method to implement in this lesson is public ExecutionResult<Collection<String>> getItemDefinitionAssetIds(String scope, String decodedItemId). This method's purpose is to find all the assets related to the decodedItemId and return them in a collection of strings. On Cortex API client application side, this method satisfies the GET - an item's assets HTTP Request.

Method Parameters:

  • String decodedItemId - Contains the item's ID This could be a UID, GUID, or something else your backend system understands. Notice the method uses the item's decoded ID Once the ID goes up the Cortex API stack, it gets encoded so client applications don't see real I.Ds. For more information, see ID Encoding.
  • String scope - Identifies the store's domain (not used in this tutorial).

	@Override
	public ExecutionResult<Collection<String>> getItemDefinitionAssetIds(
			String scope, String decodedItemId) {
		
		ExecutionResult<Collection<String>> result;
		Collection<String> assetIds = new ArrayList<String>();
		assetIds.add("1");
		assetIds.add("2");
		assetIds.add("3");
		
		result = ExecutionResultFactory.createReadOK(assetIds);
		
		return result;
	}

Here's how the code flows in the snippet:

1. Get the list of assets

We don't actually get the list of asset I.Ds from a backend system in this sample, we hard-code the IDs and pass them back. If you were coding your integration resource talk to your backend system, you would use the decodedItemId String to retrieve the list from your backend system and add the results to the assetIds String collection.

2. Set the Operation's Status

Set the operation's status using the ExecutionResultFactory. In our tutorial , we are setting it to ExecutionResultFactory.createReadOK(). If the operation failed or something else happened, you would set a different status here.

ExecutionResult<AssetDto> result = ExecutionResultFactory.createReadOK(assetIds);

3. Return the Result

Lastly, return the collection in an ExecutionResult. We wrap the collection in an ExecutionResult because we do not throw exceptions in the Cortex API. Instead of throwing an exception, we return an ExecutionResult with a ResourceStatus to indicate the operation's success or failure. Indicating the operation's status this way, instead of throwing an exception with a convoluted stack trace, allows the Cortex API to fail gracefully.

Export your Code for OSGi to Consume

You need to export your code as a service so OSGi can consume it.

Why do we need OSGi to consume our code?

Inside theCortex API's Web App, Cortex API runs in an OSGi framework. We need to export our code in a way OSGi can be aware of it and consume it.

To export your code for OSGi:

Declare the following in your project's OSGI-INF/blueprint/example-integration-blueprint.xml:

<blueprint
		xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
	...

	<service ref="assetLookupStrategy"
		interface="com.elasticpath.rest.resource.assets.integration.AssetLookupStrategy" />
	
</blueprint>

This exports the bean named assetLookupStrategy, which you defined above in Name your Implementation Bean, along with interface com.elasticpath.rest.resource.assets.integration.AssetLookupStrategy as a service OSGi can consume.

Next, add the following declaration to your project's spring/applicationContext-example-integration.xml:

<beans>
	...
	<context:component-scan base-package="com.elasticpath.tutorials" scope-resolver="org.springframework.context.annotation.Jsr330ScopeMetadataResolver"/>
</beans>

This declaration tells Spring to scan your resource integration for annotation-based injections, which we used in the Name your Implementation Bean lesson. For more information on Spring classpath scanning, see http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/beans.html#beans-classpath-scanning.

After you've made these changes, you'll build your resource integration project and install it in your local Maven repository. From a command line, change directories to the root of your integration project and execute:

mvn install

Override the Old Resource Integration with the New Resource Integration

This lesson teaches you how to override the old resource integration with the new one you created in this tutorial. To make this change, you'll need to shut down your Cortex API web application, modify the web application's POM file, and then restart the web application.

Warning: Before You Continue

These steps assume you have installed your new resource integration into your Maven repository. See the lesson Export your Code for OSGi to Consume above for how to do this.

In this lesson, you are modifying the following XML elements in the Cortex API web application's POM file that you created in Generate Cortex Projects.

  • <dependency> - Tells Maven to include your integration project's jar as a dependency during build.
  • <execution> - Execution groups configure the goals for the plugin. In this tutorial, we are going to configure the Cortex API Web App POM to override the out-of-the-box resource asset's <execution> group. For more information on POMs, see Cortex Web Application POMs.

To override the out-of-the-box assets resource integration:

  1. With a text editor, open your Cortex API web application's POM file.
  2. Add your example-assets-resource-integration as a <dependency> and configure the :copy-assets-resource-and-integration-OSGi-bundles <execution> group following the example below.
    ...
    <dependencies>
    		<!-- Add Extension resources or Integration modules as maven dependencies here. -->
    		<dependency>
    			<groupId>com.elasticpath.tutorials</groupId>
    			<artifactId>example-assets-resource-integration</artifactId>
    			<version>${project.version}</version>
    			<scope>provided</scope>
    		</dependency>
    </dependencies>
    ...
    <plugin>
    				<groupId>org.apache.maven.plugins</groupId>
    				<artifactId>maven-dependency-plugin</artifactId>
    				<executions>
    					<!-- Override Assets CE Integration and use an example integration instead. -->
    					<execution>
    						<id>copy-assets-resource-and-integration-OSGi-bundles</id>
    						<phase>prepare-package</phase>
    						<goals>
    							<goal>copy</goal>
    						</goals>
    						<configuration>
    							<outputDirectory>${war-bundle-directory}</outputDirectory>
    							<artifactItems>
    								<artifactItem>
    									<groupId>com.elasticpath.rest.resource</groupId>
    									<artifactId>ep-resource-assets</artifactId>
    								</artifactItem>
    								<artifactItem>
    									<groupId>com.elasticpath.tutorials</groupId>
    									<artifactId>example-assets-resource-integration</artifactId>
    								</artifactItem>
    							</artifactItems>
    						</configuration>
    					</execution>

    What the heck did I just configure in the POM?

    You might be wondering what's going on in the POM that you just configured, or you may be a Maven expert and you already know, which in that case please skip to the next step. By adding example-assets-resource-integration as a dependency, you are telling Maven to include this project in the build. The <execution> group you configured is

    This means that the dependency, a JAR file called example-assets-resource-integration-*.jar, builds to your cortex-dce-webapp-*.war\WEB-INF\bundles directory.

  3. From a command line, change directories to your Cortex API Web Application folder and execute:
    mvn clean install -DskipAllTests

Run and Test the New Resource Integration

Out of the box, the Avatar product doesn't have an asset. Now that we've coded the new assets resource integration to return an asset for Avatar, you should see the following JSON objects when you get Avatar's assets.

Note: Starting Up Search Note

The search we perform in the second step is only necessary if you haven't started up your Cortex API web application before. We need Search to index our product item IDs, or we will be unable to retrieve products.

To run and test the new resource integration:

  1. Start up your Cortex API web application.
  2. Using a REST client, log into the demo store by following the instructions here: http://api-cortex-developers.docs.elasticpath.com/drupal/content/Authenticate_a_customer using Username: oliver.harris@elasticpath.com and password: password.
  3. Using a REST client, create a search by sending the following request:
    Table 3.
    HTTP Request Method URL Header Body
    POST https://localhost:8443/cortex/searches/mobee/keywords/items Content-type: Application/json {"keywords":"avatar"}
  4. Now make the following call to retrieve the list of assets for the avatar product. Response
    {
      "self": {
        "type": "application/vnd.elasticpath.links",
        "uri": "/assets/itemdefinitions/mobee/mvsdazbtheytgnjyhe3tizrygq2wcmzrmnrdsnrvmq2dgyrxhe2dkn3cmy2tindd",
        "href": "http://localhost:8080/cortex/assets/itemdefinitions/mobee/mvsdazbtheytgnjyhe3tizrygq2wcmzrmnrdsnrvmq2dgyrxhe2dkn3cmy2tindd",
        "max-age": 600
      },
      "links": [
        {
          "type": "application/vnd.elasticpath.image",
          "rel": "element",
          "href": "http://localhost:8080/cortex/assets/mobee/ge=",
          "uri": "/assets/mobee/ge="
        },
        {
          "type": "application/vnd.elasticpath.image",
          "rel": "element",
          "href": "http://localhost:8080/cortex/assets/mobee/gi=",
          "uri": "/assets/mobee/gi="
        },
        {
          "type": "application/vnd.elasticpath.image",
          "rel": "element",
          "href": "http://localhost:8080/cortex/assets/mobee/gm=",
          "uri": "/assets/mobee/gm="
        },
        {
          "type": "application/vnd.elasticpath.itemdefinition",
          "rel": "definition",
          "rev": "assets",
          "href": "http://localhost:8080/cortex/itemdefinitions/mobee/mvsdazbtheytgnjyhe3tizrygq2wcmzrmnrdsnrvmq2dgyrxhe2dkn3cmy2tindd",
          "uri": "/itemdefinitions/mobee/mvsdazbtheytgnjyhe3tizrygq2wcmzrmnrdsnrvmq2dgyrxhe2dkn3cmy2tindd"
        }
      ]
    }
  5. Now make the following call to retrieve the asset for the avatar product.
    Table 5.
    HTTP Request Method URL Header
    GET https://localhost:8443/cortex/assets/mobee/ge= Content-type: Application/json
    Response
    {
      "self": {
        "type": "application/vnd.elasticpath.image",
        "uri": "/assets/mobee/ge=",
        "href": "http://localhost:8080/cortex/assets/mobee/ge=",
        "max-age": 600
      },
      "links": [],
      "content-location": "http://mobee.elasticpath.com/avatar.jpg",
      "display-name": "avatar",
      "name": "A12345",
      "relative-location": "avatar.jpg"
    }

Troubleshooting

A number of issues can occur when running this tutorial. The list below shows some of the more common errors and suggestions for solving them.

Webapp Startup Error

Cortex API Web App fails to start up with the following error:

RELOS: 6392   [rThread-25] ERROR pendencyWaiterApplicationContextExecutor - Unable to create application context for [com.elasticpath.tutorials.example-assets-resource-integration], unsatisfied dependencies: none
org.springframework.beans.factory.BeanCreationException: Error creating bean with name '.org.eclipse.gemini.blueprint.service.exporter.support.OsgiServiceFactoryBean#0': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Cannot locate bean named 'assetLookupStrategy' inside the running bean factory.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1422) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:518) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:455) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:293) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:290) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:192) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:567) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:895) ~[spring-context-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.eclipse.gemini.blueprint.context.support.AbstractDelegatedExecutionApplicationContext.access$1600(AbstractDelegatedExecutionApplicationContext.java:60) ~[na:na]
	at org.eclipse.gemini.blueprint.context.support.AbstractDelegatedExecutionApplicationContext$4.run(AbstractDelegatedExecutionApplicationContext.java:325) ~[na:na]
	at org.eclipse.gemini.blueprint.util.internal.PrivilegedUtils.executeWithCustomTCCL(PrivilegedUtils.java:85) ~[na:na]
	at org.eclipse.gemini.blueprint.context.support.AbstractDelegatedExecutionApplicationContext.completeRefresh(AbstractDelegatedExecutionApplicationContext.java:290) ~[na:na]
	at org.eclipse.gemini.blueprint.extender.internal.dependencies.startup.DependencyWaiterApplicationContextExecutor$CompleteRefreshTask.run(DependencyWaiterApplicationContextExecutor.java:137) ~[na:na]
	at java.lang.Thread.run(Thread.java:662) [na:1.6.0_30]
Caused by: java.lang.IllegalArgumentException: Cannot locate bean named 'assetLookupStrategy' inside the running bean factory.
	at org.springframework.util.Assert.isTrue(Assert.java:65) ~[spring-core-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.eclipse.gemini.blueprint.service.exporter.support.OsgiServiceFactoryBean.afterPropertiesSet(OsgiServiceFactoryBean.java:190) ~[na:na]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1479) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1419) ~[spring-beans-3.0.7.RELEASE.jar:3.0.7.RELEASE]
	... 14 common frames omitted

This error could be caused by one of the following:

  • The base-package in the applicationContext-example-integration.xml isn't set properly for OSGi. For more information, see Export your Code for OSGi to Consume.
  • The bean annotation @Named("assetLookupStrategy") may not be defined for your AssetLookupStrategyImpl class. For more information, see Name your Implementation Bean.