Port of schuchert.wikispaces.com


EJB3_Tutorial_4_Extended_Context

EJB3_Tutorial_4_Extended_Context

Ejb3 Tutorial 4 - Extended Context

A Stateful session bean can optionally use an extended context. An extended context maintains its managed objects between transactions or even in situation where a method is not using transactions. All objects accessed or created hang around until the bean goes away. This normally happens when a client executes a method that has been denoted as a Remove method (annotated with @Remove or declared as such in XML).

This short tutorial demonstrates some of the differences between these two types of container-managed contexts.

Project Setup

The instructions for setting up your project mirror those from the first tutorial: EJB3_Tutorial_1_Create_and_Configure.

For the remainder of this tutorial, when you see ****, replace it with **Ejb3Tutorial4**.


title: Ejb3EclipseProjectSetupAndConfiguration —

Create the Project

  • Pull down the File menu and select new:project
  • Select Java Project
  • Click Next
  • Enter ****
  • Under Project Layout select create separate source and output folders
  • Click Finish
  • Select ****, right-click and select **new:Source Folder**
  • Enter conf for the name
  • Click on Finish
  • Select ****, right-click and select **new:Source Folder**
  • Enter test for the name
  • Click on Finish

Edit your project properties

Now that we have created a user library, we can add that user library to our project:

  • Select ****, and press alt-enter or right-click and select properties.
  • Select Java Build Path
  • Select the Libraries tab
  • Click on Add Library
  • Select User Library and click Next
  • Click on the box next to EJB3_EMBEDDABLE and click Finish
  • Click Add Library
  • Select JUnit and click Next
  • In the pull-down list, select JUnit 4 and click Finish
  • Click on OK to make the change to your project’s classpath

Setup the configuration files

The JBoss Embeddable container looks for several files in the classpath. To keep all of these in one place, we’ll add another source directory to our project and then import several files into that directory.

  • Select the conf folder under ****
  • Pull down the File menu and select Import
  • Expand General
  • Select File System and click on Next
  • Click on Browse and go to the following directory: C:/libs/jboss-EJB-3.0_Embeddable_ALPHA_9/conf
  • Click on OK
  • You’ll see conf in the left pane, select it
  • Verify that the Into folder: lists **/conf** (if not, just enter it or browse to it)
  • Click Finish
  • Expand the conf directory and verify that the files are now there

Add Resource Adapter Archive(RAR)

The Java Connector system defines Resource Adapter Archive files (RAR files). We need to add a few RAR files into the class path. We will import two more files into the conf directory:

  • Select the conf folder
  • Pull down the File menu and select Import
  • Expand General
  • Select File System and click on Next
  • Click on Browse and go to the following directory: C:/libs/jboss-EJB-3.0_Embeddable_ALPHA_9/lib
  • Select jcainflow.rar and jms-ra.rar
  • Click Finish

Create a jndi.properties file

Note, depending on the version of the embeddable container you download, you might already have a file called jndi.properties. If you do, skip to the next section.

  • Select the conf directory, right-click and select new then select File
  • Enter the name jndi.properties and click finish
  • Enter the following 2 lines then save and close the file:
java.naming.factory.initial=org.jnp.interfaces.LocalOnlyContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces

Create a persistence.xml

This example presents a utility class we’ll be using later. The container needs a persistence.xml file to operate. This file must be found under a META-INF directory somewhere in the classpath or the embeddable container will not start. The file’s name is persistence.xml with a lower-case ‘p’. On a Unix system, this will make a difference. On a PC, this won’t make a difference and it is one of those things that might work on your machine but not on the linux build box.

  • Select your src directory
  • Right-click, select New:Folder
  • Enter META-INF
  • Click OK
  • Select META-INF
  • Right-lick, select New:File
  • Enter persistence.xml
  • Click Finish
  • Copy the following example into your new file then save it by pressing ctrl-s

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence>
   <persistence-unit name="custdb">
   
    <!-- This persistence unit uses the default data source that JBoss    -->
    <!-- defines called DefaultDS. If we wanted to use our own data       -->
    <!-- source, we'd need to define a custom data source somewhere.      -->
    <!-- That somewhere is vendor specific.                               -->
    
    <!-- In the case of JBoss, since we're using the embedded container,  -->
    <!-- we need to add two beans in a file called                        -->
    <!-- embedded-jboss-beans.xml. We name the first                      -->
    <!-- HypersonicLocalServerDSBootstrap and we name the second          -->
    <!-- HypersonicLocalServerDS. This two step process defines a data    -->
    <!-- source.                                                          -->
    
    <!-- In the first bean definition, we additionally bind it in Jndi    -->
    <!-- under some name. If we used the name                             -->
    <!-- java:/HypersonicLocalServerDS then we would use the following    -->
    <!-- entry to use that data source instead of the default one:        -->
    <!-- <jta-data-source>java:/HypersonicLocalServerDS</jta-data-source> -->
 
      <jta-data-source>java:/DefaultDS</jta-data-source>
      <properties>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      </properties>
   </persistence-unit>
</persistence>

Here are a few things to note (source for all of these items appears at the end after the assignments EJB3_Tutorial_4_Extended_Context#OtherFiles:

<persistence-unit name="tolltag">

The Entity Model

For this example, we have a simple entity model. We have an Account that has a bidirectional one-to-many relationship with TollTag objects and a bidirectional one-to-many relationship with Vehicle objects. Normally, one-to-many relationships are lazily fetched. For this example, the relationship with TollTag objects is left as lazily fetched while the relationship with Vehicle objects is eagerly fetched.

Account.java

package entity;

import java.util.ArrayList;
import java.util.Collection;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

/**
 * This is a simple Account class that knows about toll tags and vehicles. An
 * account has a one to many relationship with both toll tags and vehicles. By
 * default, one to many relationships are lazily loaded. To demonstrate
 * differences between extended scope contexts and transaction-scoped contexts,
 * one of these relationships is eagerly fetched.
 */
@Entity
public class Account {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * This relationship is lazily fetched. This means a client using a detached
     * Account will not be able to access tollTags unless the relationship was
     * touched while the object was still managed.
     */
    @OneToMany(mappedBy = "account", cascade = CascadeType.ALL)
    private Collection<TollTag> tollTags;

    /**
     * We eagerly fetch this relationship to show that doing so allows this
     * relationship to work if the object is or is not detached. NOTE: only
     * one "Collection" can have a fetch property of EAGER. If you want to
     * to use fetch = FetchType.EAGER more than once in the same class, the
     * other "Collections" will have to be "Set"s.
     */
    @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, 
               fetch = FetchType.EAGER)
    private Collection<Vehicle> vehicles;

    public Account() {
    }

    public Long getId() {
        return id;
    }

    public Collection<TollTag> getTollTags() {
        if (tollTags == null) {
            tollTags = new ArrayList<TollTag>();
        }

        return tollTags;
    }

    public Collection<Vehicle> getVehicles() {
        if (vehicles == null) {
            vehicles = new ArrayList<Vehicle>();
        }

        return vehicles;
    }

    public void addVehicle(final Vehicle vehicle) {
        getVehicles().add(vehicle);
        vehicle.setAccount(this);
    }

    public void removeVehicle(final Vehicle vehicle) {
        getVehicles().remove(vehicle);
        vehicle.setAccount(null);
    }

    public void addTollTag(final TollTag tollTag) {
        getTollTags().add(tollTag);
        tollTag.setAccount(this);
    }

    public void removeTollTag(final TollTag tt) {
        getTollTags().remove(tt);
        tt.setAccount(null);
    }
}

TollTag.java

package entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

import util.EqualsUtil;

/**
 * For some hard to decipher results, change the "FROM TollTag t" to "FROM
 * TollTag".
 */
@Entity
@NamedQueries( {
    @NamedQuery(name = "TollTag.associatedAccount", 
        query = "SELECT t.account FROM TollTag t WHERE tagNumber = :tagNumber"),
    @NamedQuery(name = "TollTag.byTolltagNumber", 
            query = "SELECT t FROM TollTag t WHERE tagNumber = :tagNumber") })
public class TollTag {
    @Id
    @GeneratedValue
    private Long id;

    @Column(unique = true)
    private String tagNumber;

    @ManyToOne
    private Account account;

    public Long getId() {
        return id;
    }

    public void setId(final Long id) {
        this.id = id;
    }

    public String getTagNumber() {
        return tagNumber;
    }

    public void setTagNumber(final String tagNumber) {
        this.tagNumber = tagNumber;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(final Account account) {
        this.account = account;
    }

    @Override
    public boolean equals(final Object rhs) {
        return rhs instanceof TollTag
                && EqualsUtil.equals(getTagNumber(), ((TollTag) rhs)
                        .getTagNumber());
    }

    @Override
    public int hashCode() {
        return getTagNumber().hashCode();
    }
}

Vehicle.java

package entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import util.EqualsUtil;

@Entity
public class Vehicle {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Account account;

    private String make;
    private String model;
    private String year;
    private String license;

    public Vehicle() {
    }

    public Vehicle(final String make, final String model, final String year,
            final String license) {
        setMake(make);
        setModel(model);
        setYear(year);
        setLicense(license);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getLicense() {
        return license;
    }

    public void setLicense(String license) {
        this.license = license;
    }

    public String getMake() {
        return make;
    }

    public void setMake(String make) {
        this.make = make;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public String getYear() {
        return year;
    }

    public void setYear(String year) {
        this.year = year;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }

    @Override
    public boolean equals(final Object object) {
        if (object instanceof Vehicle) {
            final Vehicle rhs = (Vehicle) object;

            return EqualsUtil.equals(getLicense(), rhs.getLicense())
                    && EqualsUtil.equals(getMake(), rhs.getMake())
                    && EqualsUtil.equals(getModel(), rhs.getModel())
                    && EqualsUtil.equals(getYear(), rhs.getYear());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return getLicense().hashCode() * getMake().hashCode()
                * getModel().hashCode() * getYear().hashCode();
    }
}

The Session Beans

AccountInventory.java

package session;

import javax.ejb.Local;

import entity.Account;

/**
 * This interface is a bit abnormal as it is being used for both a stateful and
 * stateless session bean. See individual method comments for clarification.
 */
@Local
public interface AccountInventory {
    void removeTag(final String tagNumber);

    Account findAccountByTagNumber(final String tagNumber);

    /**
     * Strictly speaking, this method is required only for transaction-managed
     * contexts. If you use a stateful session bean with an extended context,
     * then changed to any managed objects will eventually be written. There's
     * no need to actually call an update method.
     * 
     * If you have a stateless session bean or a stateful session bean using a
     * transaction-scoped context, then you need to call an update method after
     * making changes to an object outside of a bean because the object is no
     * longer managed.
     */
    Account updateAccount(final Account account);

    /**
     * When do updates happen to objects managed by an extended-context manager?
     * Answer, when the client calls a so-called remove method (annotated with
     * -at- Remove or denoted so in an XML file).
     * 
     * This method serves no purpose for a stateless session bean. For a
     * stateful session bean using an extended context, when the client calls
     * this method, the container knows it is time to write all of the changes
     * it has been tracking to the database.
     * 
     */
    void finish();

    Account findAccountById(final Long id);

    void removeAccount(final Account account);

    void createAccount(final Account account);
}

AccountInventoryBean.java

package session;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import entity.Account;
import entity.TollTag;

@Stateless
public class AccountInventoryBean implements AccountInventory {
    @PersistenceContext(unitName = "tolltag")
    private EntityManager em;

    public EntityManager getEm() {
        return em;
    }

    public void createAccount(final Account account) {
        getEm().persist(account);
    }

    public Account findAccountById(final Long id) {
        return getEm().find(Account.class, id);
    }

    public void removeTag(final String tagNumber) {
        final TollTag tt = (TollTag) getEm().createNamedQuery(
                "TollTag.byTollTagNumber").setParameter("tagNumber", tagNumber)
                .getSingleResult();
        final Account account = tt.getAccount();
        account.removeTollTag(tt);
        tt.setAccount(null);
        getEm().remove(tt);
        getEm().flush();
    }

    public Account findAccountByTagNumber(final String tagNumber) {
        return (Account) getEm().createNamedQuery("TollTag.associatedAccount")
                .setParameter("tagNumber", tagNumber).getSingleResult();
    }

    public Account updateAccount(final Account account) {
        return getEm().merge(account);
    }

    public void finish() {
        // Do nothing, I'm really for the extended example
    }

    public void removeAccount(final Account account) {
        final Account toRemove = getEm().merge(account);
        getEm().remove(toRemove);
        getEm().flush();
    }
}

AccountInventoryExtendedBean.java

package session;

import javax.ejb.Remove;
import javax.ejb.Stateful;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;

import entity.Account;
import entity.TollTag;

@Stateful
public class AccountInventoryExtendedBean implements AccountInventory {
    @PersistenceContext(unitName = "tolltag", 
                        type = PersistenceContextType.EXTENDED)
    private EntityManager extendedEm;

    public EntityManager getEm() {
        return extendedEm;
    }

    public Account findAccountById(final Long id) {
        return getEm().find(Account.class, id);
    }

    public Account findAccountByTagNumber(final String tagNumber) {
        return (Account) getEm().createNamedQuery("TollTag.associatedAccount")
                .setParameter("tagNumber", tagNumber).getSingleResult();
    }
    
    public void createAccount(final Account account) {
        getEm().persist(account);
    }

    public Account updateAccount(final Account account) {
        return account;
    }

    @Remove
    public void finish() {
    }

    public void removeTag(String tagNumber) {
        final TollTag tt = (TollTag) getEm().createNamedQuery(
                "TollTag.byTollTagNumber").setParameter("tagNumber", tagNumber)
                .getSingleResult();
        final Account account = tt.getAccount();
        account.removeTollTag(tt);
        tt.setAccount(null);
        getEm().remove(tt);
        getEm().flush();
    }

    public void removeAccount(final Account account) {
        getEm().remove(account);
        getEm().flush();
    }
}

The Tests

This class performs the same two test algorithms two times each for a total of 4 test methods:

Name Scope Accesses Expected
createExampleUsingVehiclesTransactionScoped Transaction Vehicles Success
createExampleUsingVehiclesExtendedScoped Extended Vehicles Success
createExampleUsingTollTagsTransactionScoped Transaction TollTags Fails
createExampleUsingTollTagsExtendedScoped Extended TollTags Success

AccountInventoryBeanTest.java

package session;
 
import static org.junit.Assert.assertEquals;
 
import org.junit.BeforeClass;
import org.junit.Test;
 
import util.JBossUtil;
import entity.Account;
import entity.TollTag;
import entity.Vehicle;
 
public class AccountInventoryBeanTest {
    @BeforeClass
    public static void initContainer() {
        JBossUtil.startDeployer();
    }
 
    public static AccountInventory getInventory() {
        return JBossUtil.lookup(AccountInventory.class,
                "AccountInventoryBean/local");
    }
 
    public static AccountInventory getExtendedInventory() {
        return JBossUtil.lookup(AccountInventory.class,
                "AccountInventoryExtendedBean/local");
    }
 
    public static TollTag instantiateTollTag() {
        final TollTag tt = new TollTag();
        tt.setTagNumber("1234567890");
        return tt;
    }
 
    public static Vehicle instantiateVehicle() {
        return new Vehicle("Subaru", "Outback", "2001", "YBU 155");
    }
 
    /**
     * This method creates an account, looks it up and then accesses the toll
     * tags relationship. The toll tags relationship is lazily loaded. If the
     * passed-in bean is one that uses a transaction-managed context, then the
     * assert will fail because the relationship has not been initialized.
     * 
     * On the other hand, if the bean is one that uses an extended persistence
     * context, then the assert will pass because the relationship, will still
     * lazily loaded, will get initialized when accessed since the account
     * object is still managed.
     */
    private void createExampleTestTollTagsImpl(final AccountInventory bean) {
        final Account account = new Account();
        account.addTollTag(instantiateTollTag());
        account.addVehicle(instantiateVehicle());
 
        bean.createAccount(account);
 
        try {
            final Account found = bean.findAccountById(account.getId());
            assertEquals(1, found.getTollTags().size());
        } finally {
            bean.finish();
            getInventory().removeAccount(account);
        }
    }
 
    /**
     * As a counter example to createExampleTestTollTagsImpl, this method
     * follows the same step but instead uses the vehicles relationship. Since
     * this relationship has been set to fetch eagerly, it is available
     * regardless of whether or not the account object is still managed.
     */
    private void createExampleTestVehiclesImpl(final AccountInventory bean) {
        final Account account = new Account();
        account.addTollTag(instantiateTollTag());
        account.addVehicle(instantiateVehicle());
 
        bean.createAccount(account);
 
        try {
            final Account found = bean.findAccountById(account.getId());
            assertEquals(1, found.getVehicles().size());
        } finally {
            bean.finish();
            getInventory().removeAccount(account);
        }
    }
 
    @Test
    public void createExampleUsingVehiclesTransactionScoped() {
        createExampleTestVehiclesImpl(getInventory());
    }
 
    @Test
    public void createExampleUsingVehiclesExtendedScoped() {
        createExampleTestVehiclesImpl(getExtendedInventory());
    }
 
    @Test
    public void createExampleUsingTollTagsTransactionScoped() {
        createExampleTestTollTagsImpl(getInventory());
    }
 
    @Test
    public void createExampleUsingTollTagsExtendedScoped() {
        createExampleTestTollTagsImpl(getExtendedInventory());
    }
}

Exercises

Test Passing

There are 3 ways to make the one failing test pass:

Experiment with each of these options.

Shared Code

There is a lot of shared code between the two AccountInventory bean implementations. Describe at least two ways you could reduce the redundancy.

Interfaces

The interface seems to be a bit messed up with concepts that relate to both stateless and stateful beans. Describe how you might change the interface to make this better. Consider using two interfaces instead of one.

Other Files

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence>
   <persistence-unit name="tolltag">
      <jta-data-source>java:/DefaultDS</jta-data-source>
      <properties>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      </properties>
   </persistence-unit>
</persistence>

title: Ejb3JBossUtilJava —

JBossUtil.java

package util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.logging.Logger;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.jboss.ejb3.embedded.EJB3StandaloneBootstrap;

/**
 * This class was originally necessary when using the ALPHA 5 version of the
 * embeddable container. With the alpha 9 release, initialization is quite
 * simple, you need just 2 lines to initialize your JBoss Embeddable EJB3
 * Container Environment. Unfortunately, the one that is available for download
 * uses System.out.println() in a few places, so this simple utility hides that
 * output and also provides a simple lookup mechanism.
 */
public class JBossUtil {
	private static PrintStream originalOut;

	private static PrintStream originalErr;

	private static OutputStream testOutputStream;

	private static PrintStream testOuputPrintStream;

	static boolean initialized;

	static InitialContext ctx;

	private JBossUtil() {
		// I'm a utility class, do not instantiate me
	}

	/**
	 * JBoss EJB3 Embeddable Container uses System.out. Redirect that output to
	 * keep the console output clean.
	 */
	private static void redirectStreams() {
		// configure the console to get rid of hard-coded System.out's in
		// the JBoss libraries
		testOutputStream = new ByteArrayOutputStream(2048);
		testOuputPrintStream = new PrintStream(testOutputStream);

		originalOut = System.out;
		originalErr = System.err;

		System.setOut(testOuputPrintStream);
		System.setErr(testOuputPrintStream);
	}

	/**
	 * Restore the System.out and System.err streams to their original state.
	 * Close the temporary stream created for the purpose of redirecting I/O
	 * while the initializing is going on.
	 */
	private static void restoreStreams() {
		System.setOut(originalOut);
		System.setErr(originalErr);
		testOuputPrintStream.close();
		try {
			testOutputStream.close();
		} catch (IOException e) {
			Logger.getLogger(JBossUtil.class.getName()).info(
					"Unable to close testoutstream");
		}
	}

	/**
	 * This method starts and initializes the embeddable container. We do not
	 * offer a method to properly clean up the container since this is really
	 * meant for testing only.
	 * 
	 * This method may freely be called as often as you'd like since it lazily
	 * initializes the container only once.
	 */
	public static void startDeployer() {
		if (!initialized) {
			redirectStreams();

			EJB3StandaloneBootstrap.boot(null);
			EJB3StandaloneBootstrap.scanClasspath();

			initialized = true;

			restoreStreams();
		}
	}

	/**
	 * This is for symmetry. Given how we are using this class, there's little
	 * need to actually shutdown the container since we run a quick application
	 * and then stop the JVM.
	 */
	public static void shutdownDeployer() {
		EJB3StandaloneBootstrap.shutdown();
	}

	private static InitialContext getContext() {
		/**
		 * We only keep one context around, so lazily initialize it
		 */
		if (ctx == null) {
			try {
				ctx = new InitialContext();
			} catch (NamingException e) {
				throw new RuntimeException("Unable to get initial context", e);
			}
		}

		return ctx;
	}

	/**
	 * The lookup method on InitialContext returns Object. This simple wrapper
	 * asks first for the expected type and the for the name to find. It gets
	 * the name out of JNDI and performs a simple type-check. It then casts to
	 * the type provided as the first parameter.
	 * 
	 * This isn't strictly correct since the cast uses the expression (T), where
	 * T is the generic parameter and the type is erased at run-time. However,
	 * since we first perform a type check, we know this cast is safe. The -at-
	 * SuppressWarnings lets the Java Compiler know that we think we know what
	 * we are doing.
	 * 
	 * @param <T>
	 *            Type type provided as the first parameter
	 * @param clazz
	 *            The type to cast to upon return
	 * @param name
	 *            The name to find in Jndi, e.g. XxxDao/local or, XxxDao/Remote
	 * @return Something out of Jndi cast to the type provided as the first
	 *         parameter.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T lookup(Class<T> clazz, String name) {
		final InitialContext ctx = getContext();
		/**
		 * Perform the lookup, verify that it is type-compatible with clazz and
		 * cast the return type (using the erased type because that's all we
		 * have) so the client does not need to perform the cast.
		 */
		try {
			final Object object = ctx.lookup(name);
			if (clazz.isAssignableFrom(object.getClass())) {
				return (T) object;
			} else {
				throw new RuntimeException(String.format(
						"Class found: %s cannot be assigned to type: %s",
						object.getClass(), clazz));
			}

		} catch (NamingException e) {
			throw new RuntimeException(String.format(
					"Unable to find ejb for %s", clazz.getName()), e);
		}
	}
}

EqualsUtil.java

package util;

/**
 * We typically need to compare two object and also perform null checking. This
 * class provides a simple wrapper to accomplish doing so.
 */

public class EqualsUtil {
    private EqualsUtil() {
        // I'm a utility class, do not instantiate me
    }

    public static boolean equals(final Object lhs, final Object rhs) {
        return lhs == null && rhs == null
                || (lhs != null && rhs != null && lhs.equals(rhs));

    }
}


Comments

" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.