Java Planet

O Javie i jej otoczeniu

Mapowanie typów w Toplink JPA

Co zrobić, aby budując system oparty o tradycyjną, relacyjną bazę danych oraz Toplink JPA wykorzystać w pełni siłę języka Java i JPQL? Tak by móc konstruować zapytania typu:

IpAddress ipAddr = new IpAddress("10.0.0.2");
TDhcp dhcp = em.createQuery("SELECT d from TDhcp d WHERE d.ipAddress=:ip")
          .setParameter("ip", ipAddr)
          .getSingleResult();

MacAddress mac = new MacAddress("00:21:70:83:ff:ff");
TDhcp dhcp2 = em.createQuery("SELECT d from TDhcp d WHERE d.macAddress=:mac")
          .setParameter("mac", mac)
          .getSingleResult();

podczas gdy w bazie danych ip zapisane jest jako zwykły, brzydki integer, a macaddress jako string ?
Nic prostszego! Możemy przygotować mechanizm mapowania.
Zapraszam więc do lektury (i komentowania) artykułu w którym postaram się przedstawić przykład wykorzystania mapowania pomiędzy bazodanowym typem integer a naszym własnym typ IpAddress.


Tabela t_dhcp:

CREATE TABLE `t_dhcp` (
  `ip_address` int(10) unsigned NOT NULL,
  `mac_address` char(12) NOT NULL,
  `lease_due` datetime default NULL,
  PRIMARY KEY  (`ip_address`)

Entity classTDhcp dla tabeli t_dhcp. W podświetlonych liniach widać jak proste jest użycie raz przygotowanego konwertera (właściwie dwóch konwerterów – dla IpAddress i MacAddress)

package eu.swierczyna.ipmanager.entity;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import eu.swierczyna.ipmanager.IpAddress;
import eu.swierczyna.ipmanager.MacAddress;
import eu.swierczyna.ipmanager.toplink.Converter;
import eu.swierczyna.ipmanager.toplink.ConverterType;

@Entity
@Table(name="t_dhcp")
public class TDhcp implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	@Column(name="ip_address", nullable=false)
	@Converter(type=ConverterType.IP_ADDRESS_CONVERTER)
	private IpAddress ipAddress;

	@Temporal( TemporalType.TIMESTAMP)
	@Column(name="lease_due")
	private Date leaseDue;

	@Column(name="mac_address")
	@Converter(type=ConverterType.MAC_ADDRESS_CONVERTER)
	private MacAddress macAddress;

	// getters and setters
	public IpAddress getIpAddress() {
		return ipAddress;
	}

	public void setIpAddress(IpAddress ipAddress) {
		this.ipAddress = ipAddress;
	}

	public Date getLeaseDue() {
		return leaseDue;
	}

	public void setLeaseDue(Date leaseDue) {
		this.leaseDue = leaseDue;
	}

	public MacAddress getMacAddress() {
		return macAddress;
	}

	public void setMacAddress(MacAddress macAddress) {
		this.macAddress = macAddress;
	}
}

Plik persistence.xml

<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
 <persistence-unit name="IpManager" transaction-type="JTA">
 <jta-data-source>jdbc/ipmanager</jta-data-source>

 <class>eu.swierczyna.prov.ipmanager.entity.TDhcp</class>
 <exclude-unlisted-classes>true</exclude-unlisted-classes>

 <properties>
    <!-- wskazanie dla Toplinka by przeglądnął klasę pod kontem występowania naszych anotacji -->
    <!-- musimy dodać taki wpis do każdej z entity class -->
    <property name="toplink.descriptor.customizer.TDhcp" value="eu.swierczyna.prov.ipmanager.toplink.DescriptorCustomizer"/>
 </properties>

 </persistence-unit>
</persistence>

Poniżej klasa IpAddress wraz z metodami których będzie używał konwerter. Metody te oczywiście nie muszą być prywatne.(Implementacja mocno uproszczona na potrzeby artykułu)

package eu.swierczyna.ipmanager;

import java.io.Serializable;

public class IpAddress implements  Serializable{

	private long value;

	public static IpAddress toIpAddress(Long value) {
		if (value == null || value.longValue()==0 ){
			return null;
		} else {
			return new IpAddress(value);
		}
	}

	public static long toNumber(IpAddress ip) {
		if (ip==null){
			return 0;
		} else {
			return ip.toNumber();
		}
	}

	public IpAddress(){
		value=0;
	}

	public IpAddress(String value){
		// TODO: implementacja pominięta dla uproszczenia przykładu
	}
	/**
	 * Get address type
	 * @return private or public
	 */
        public String getType(){
		// TODO: implementacja pominięta dla uproszczenia przykładu
		return "private";
	}

	/**
	 * @param val ip adres w postaci liczby long. Choć tak naprawdę to liczba 32 bit, czyli integer ale w java mamy tylko signed int (od -2^31  do 2^31-1)
	 */
	public IpAddress(long val){
		if (val<0 || val>(Math.pow(2,32))){
			throw new IllegalArgumentException("Niepoprawna wartość adresu ip: "+val);
		}
		this.value = val;
	}

	public long toNumber(){
		return value;
	}
}

Implementacja klasy zapewniającej obukierunkowe mapowanie bazodanowego typu integer na nasz własny, javowy typ IpAddress. Ilementujemy dwie metody w interfejsie Converter dostarczanym przez toplink. Każda metoda dopowiada za mapowanie w jednym kierunku.

package eu.swierczyna.ipmanager.toplink;

import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.converters.Converter;
import oracle.toplink.essentials.sessions.Session;
import eu.swierczyna.ipmanager.IpAddress;

public class IpAddressConverter implements Converter {

	/**
	 * Konwersja z typu bazodanowego na typ java
	 */
	@Override
	public Object convertDataValueToObjectValue(Object data, Session arg1) {
		if (data==null){
			return null;
		}
		if (data instanceof Long){
			IpAddress result = IpAddress.toIpAddress((Long)data);
			return result;
		} else {
			throw new IllegalArgumentException("We need "+Long.class.getCanonicalName()+" as input");
		}
	}
	/**
	 * Konwersja z typu java na typ bazodanowy
	 */
	@Override
	public Object convertObjectValueToDataValue(Object object, Session arg1) {
		if (object==null){
			return null;
		}
		if (object instanceof IpAddress){
			Long result = IpAddress.toNumber((IpAddress)object);
			return result;
		} else {
			throw new IllegalArgumentException("We need "+IpAddress.class.getCanonicalName()+" as input");
		}
	}

	@Override
	public boolean isMutable() {
		return false;
	}
}

Aby spiąć klasę konwertującą IpAddressConverter z naszym typem IpAddress, tak by możliwe było używanie anotacji: @Converter(type=ConverterType.IP_ADDRESS_CONVERTER) musimy zaimplementować jeszcze trzy klasy. O ile pierwsza z nich: ConverterType wymaga dostosowana do naszego własnego projektu, o tyle pozostałe (Converter, DescriptorCustomizer) są w zasadzie uniwersalne i mogą zostać przeklejone do innych projektów.

package eu.swierczyna.ipmanager.toplink;

public enum ConverterType {
	MAC_ADDRESS_CONVERTER(MacAddressConverter.class),
	IP_ADDRESS_CONVERTER(IpAddressConverter.class);

	private Class clazz;

	ConverterType(Class clazz) {
		this.clazz = clazz;
	}

	public Class getConverter() {
		return clazz;
	}
}
package eu.swierczyna.ipmanager.toplink;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target( { FIELD, METHOD })
public @interface Converter {
	ConverterType type();
}</pre>
<pre>
package eu.swierczyna.ipmanager.toplink;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import oracle.toplink.essentials.descriptors.ClassDescriptor;
import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.DirectToFieldMapping;

public class DescriptorCustomizer  implements oracle.toplink.essentials.tools.sessionconfiguration.DescriptorCustomizer {

	public DescriptorCustomizer() {
	}

	public void customize(ClassDescriptor desc) throws Exception {
		List<DatabaseMapping> mappings = desc.getMappings();
		for (DatabaseMapping mapping : mappings) {
			if (mapping instanceof DirectToFieldMapping) {
				DirectToFieldMapping directMapping = (DirectToFieldMapping) mapping;
				String attributeName = mapping.getAttributeName();
				Class mappedClazz = mapping.getDescriptor().getJavaClass();
				checkFields(directMapping, attributeName, mappedClazz);
				checkMethods(directMapping, attributeName, mappedClazz);
			}
		}

	}

	private void checkMethods(DirectToFieldMapping mapping, String attributeName, Class clazz) throws InstantiationException, IllegalAccessException {
		Method[] methods = clazz.getDeclaredMethods();
		for (Method method : methods) {
			if (method.getName().equals(mapping.getGetMethodName())
					|| method.getName().equals(mapping.getSetMethodName())) {
				Annotation[] annons = method.getAnnotations();
				checkAnnonations(mapping, annons);
			}
		}
	}

	private void checkFields(DirectToFieldMapping directMapping, String attributeName, Class clazz) throws InstantiationException, IllegalAccessException {
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			if (field.getName().equals(attributeName)) {
				Annotation[] annotations = field.getAnnotations();
				checkAnnonations(directMapping, annotations);
			}
		}
	}

	private void checkAnnonations(DirectToFieldMapping directMapping, 	Annotation[] annotations) throws InstantiationException,  IllegalAccessException {
		for (Annotation annon : annotations) {
			if (annon.annotationType().equals(Converter.class)) {
				Converter convAnnon = (Converter) annon;
				ConverterType type = convAnnon.type();
				Class converterClass = type.getConverter();
				directMapping.setConverter((oracle.toplink.essentials.mappings.converters.Converter) converterClass.newInstance());
			}
		}
	}
}

Poniżej zestaw klas koniecznych do wykonania mapowania bazodanowego varchar’a na nasz własny typ Java – MacAddress. Zestaw klas oraz ich implementacja jest analogiczna dla mapowania int na IpAddress

 

Klasa MacAddress z rozbudowaną funkcjonalnością konwersji z/do stringów w różnej postaci.

package eu.swierczyna.ipmanager;

import java.io.Serializable;
import java.util.Arrays;

/**
 * Reprezentacja adresu MAC
 *
 * @author sebastian@swierczyna.eu
 * @since 2010-01-25
 */
public class MacAddress implements Serializable {
	private static final long serialVersionUID = -3207326186299917122L;

	private static final int LEN = 6;

	private int [] value = new int[LEN];

	public MacAddress(){
		for (int i=0;i<LEN;i++){
			value[i]=0;
		}
	}

	public static MacAddress toMac(String mac) {
		if (mac==null) {
			return null;
		} else {
			return new MacAddress(mac);
		}
	}

	public static String toStringSimple(MacAddress mac) {
		if (mac==null) {
			return null;
		} else {
			return mac.toStringSimple();
		}
	}

	public MacAddress(String val){
		if (val==null){
			throw new IllegalArgumentException("Argument can't be null");
		} else {
			String mac = val.replaceAll(":", "").replaceAll("-", "").replaceAll("\\.", "").replaceAll("\\s", ""). toUpperCase();
			if (mac.length()!=LEN*2){
				throw new IllegalArgumentException("Incorrect lenght value: "+val);
			}
			for (int i=0;i<LEN;i++){
				String x = mac.substring(i*2, (i+1)*2);
				try {
					value[i]=Integer.parseInt(x,16);
				} catch (NumberFormatException e) {
					throw new IllegalArgumentException("Argument: "+val+" contains incorrect value: "+x);
				}
			}
		}
	}

	/**
	 * return mac as xx:xx:xx:xx:xx:xx
	 */
	public String toString(){
		StringBuffer buffer = new StringBuffer();
		for (int i=0;i<LEN;i++){
			String s =Integer.toHexString(value[i]).toUpperCase();
			if (s.length()==1){
				buffer.append('0');
			}
			buffer.append(s);
			if (i<LEN-1){
				buffer.append(":");
			}
		}
		return new String(buffer);
	}

	/**
	 * return mac as xxxxxxxxxxxx
	 */
	public String toStringSimple(){
		return toString().replaceAll(":", "");
	}

	/**
	 * return mac as xxxx.xxxx.xxxx
	 */
	public String toStringCisco(){
		String c = toStringSimple().toLowerCase();
		return c.substring(0, 4)+'.'+c.substring(4, 8)+'.' +c.substring(8, 12);
	}

	public static void main(String args[]){
		MacAddress m = new MacAddress("00:20.40-c5 62 fe");
		System.out.println(m);
		System.out.println(m.toStringSimple());
		System.out.println(m.toStringCisco());
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(value);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		MacAddress other = (MacAddress) obj;
		if (!Arrays.equals(value, other.value))
			return false;
		return true;
	}
}

Oraz klasa konwertera varchar <-> MacAddress

package eu.swierczyna.ipmanager.toplink;

import oracle.toplink.essentials.mappings.DatabaseMapping;
import oracle.toplink.essentials.mappings.converters.Converter;
import oracle.toplink.essentials.sessions.Session;
import org.apache.log4j.Logger;
import eu.swierczyna.ipmanager.MacAddress;

/**
 * Converter for Toplink JPA
 * Automaticly convert between java MacAddress objects to database String objects
 *
 * @author sebastian@swierczyna.eu
 * @since 2010-02-26
 */
public class MacAddressConverter implements Converter {
	private static final long serialVersionUID = -7142128600764340149L;

	private static Logger log = Logger.getLogger(MacAddressConverter.class);

	@Override
	public Object convertDataValueToObjectValue(Object data, Session arg1) {
		if (data==null){
			return null;
		}
		log.trace("db->java. Input class:  [" + data.getClass().getCanonicalName()+ "], value: ["+ data+"]");
		if (data instanceof String){
			MacAddress result = MacAddress.toMac((String)data);
			log.trace("returning:  [" + result.getClass().getCanonicalName()+ "], value: ["+ result+"]");
			return result;
		} else {
			throw new IllegalArgumentException("We need "+String.class.getCanonicalName()+" as input");
		}
	}

	@Override
	public Object convertObjectValueToDataValue(Object object, Session arg1) {
		if (object==null){
			return null;
		}
		log.trace("java->db. Input class:  [" + object.getClass().getCanonicalName()+ "], value: ["+ object+"]");
		if (object instanceof MacAddress){
			String result = MacAddress.toStringSimple((MacAddress)object);
			log.trace("returning:  [" + result.getClass().getCanonicalName()+ "], value: ["+ result+"]");
			return result;
		} else {
			throw new IllegalArgumentException("We need "+MacAddress.class.getCanonicalName()+" as input");
		}
	}

	@Override
	public void initialize(DatabaseMapping mapping, Session arg1) {
		log.trace("Appling for table: "+mapping.getDescriptor().getDefaultTable().getName());
	}

	@Override
	public boolean isMutable() {
		return false;
	}
}

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

COMMENTS

No Comments

There are no comments posted yet. Be the first one!

Leave a Replay