Posts tagged ‘apache’

Ostatnio podczas pracy nad jednym projektem w wielu miejscach w kodzie należało przepisać przychodzące żadanie z jednego transfer object’a na drugiego (powody tegoż przepisywania są dla tego artykułu zupełnie nieistotne) w związku z czym w wielu miejscach w kodzie pojawiły się elementy do siebie łudząco podobne:

target.setSomeProperty(source.getSomeProperty())

Co oczywiście nasunęło mi myśl czy nie dałoby się tego jakoś wydzielić, ale ograniczała mnie myśl, że java to przecież nie taki dajmy na to JavaScript w którym takie figle to chleb powszedni.

Rozwiązanie podsunął mi dopiero Tomek swoim artykułem nt. BeanUtils – wystarczy przecież pobrać własności z jednego beana za pomocą metody describe i następnie korzystając z metody populate je zaaplikować – no i niby wszystko cacy, ale jest parę niuansów które mi przeszkadzają:

  • całe api jest statyczne…
  • dodawanie Converter‚ów jakioś tak topornie wygląda
  • nie ma możliwości definiowania własności, która zmieniła nazwę pomiędzy Beana’mi
  • i jak śię jakaś własność nie przepisze, to nie dowiemy się o tym dopóki nie będziemy jej próbowali pobrać, czyli znaaacznie za późno

Stąd pomysł na klasę BeanTransformer, która powinna umożliwiać:

  • definiowanie własności ze zmienioną nazwą
  • definiowanie własności do pominięcia
  • definiowanie Coverter’a z wykorzystaniem generycznego interfejsu, który sam będzie przenosił informację o typie dla jakiego ma być aplikowany
  • sprzątanie konwerterów po zakończonej konwersji

I tak właśnie narodziła się klasa BeanConverter, którą omówię poprzez opis jej metod i klas wewnętrznych upraszczających całą zabawę 🙂

No to zacznijmy od początku – zadeklarujmy klasę:


public class BeanTransformer {

}

Przydałaby się metoda pozwalająca definiować własności, które nie bedą nam potrzebne i przechowująca je w Set‚cie:

	private Set<String> skipProperites = new HashSet<String>();
	public BeanTransformer skip(String firstProperty, String... propertyNames) {
		skipProperites.add(firstProperty);
		if (propertyNames != null) {
			skipProperites.addAll(Arrays.asList(propertyNames));
		}
		return this;
	}

Jak widać przy okazji umożliwia chain’y i dodawanie więcej niż jednej własności za jednym zamachem.

Następnie potrzebujemy metodę pozwalającą zdefiniować własności, których nazwa się zmieniła:

	private Map<String, String> renameProperties = new HashMap<String, String>();
	public BeanTransformer rename(String fromProperty, String toProperty) {
		renameProperties.put(fromProperty, toProperty);
		return this;
	}

I teraz wystarczy przejechać się po własnościach, sprawdzić które na które przechodza, usunąć zbędne i sprawdzić czy coś zostało i ew. zgłosić błąd.

	private Set<String> filterCorrectProperties(final Map<String, Object> properties) {
		// first remove properties that should be skipped
		properties.keySet().removeAll(skipProperites);

		// than go over the rest of them
		Set<String> foundProperties = Sets.filter(renameProperties.keySet(),
				new Predicate<String>() {
					public boolean apply(String fromProperty) {
						if (properties.containsKey(fromProperty)) {
							Object v = properties.remove(fromProperty);
							String newKey = renameProperties.get(fromProperty);
							properties.put(newKey, v);
							return false;
						}
						return true;
					}
				});

		return foundProperties;
	}

Teraz gdy już mamy listę przefiltrowanych własności możemy je przepisać do docelowego beana

	@SuppressWarnings("unchecked")
	public <F, T> T transform(F fromBean, T toBean) throws NotUsedPropertiesException {
		try {
			Map<String, Object> properties = (Map<String, Object>) PropertyUtils.describe(fromBean);

			Set<String> foundProperties = filterCorrectProperties(properties);

			if (!foundProperties.isEmpty()) {
				throw new NotUsedPropertiesException(foundProperties);
			}

			BeanUtils.populate(toBean, properties);

			return toBean;
		} catch (IllegalAccessException e) {
			throw new NotUsedPropertiesException(e);
		} catch (InvocationTargetException e) {
			throw new NotUsedPropertiesException(e);
		} catch (NoSuchMethodException e) {
			throw new NotUsedPropertiesException(e);
		}
	}

Zakładamy, że reszta własności przepisuje się 1-1.

Jeszcze miłą opcją byłaby możliwość dodania własnego converter’a dla typów innych niż standardowe – tutaj skorzystam również z klasy szablonowej, która będzie rozszerzać interfejs Converter, ale dodatkowo definicja zawiera typ klasy który dany konwerter obsługuje, co daje nam 2 zalety:

  • Klasa niesie cały zasób informacji nt. konwersji
  • Część wspólnej logiki może być wydzielona na zewnątrz

Zacznijmy od samego interfejsu, który będzie prosty do bólu:

	public static interface Converter<T> extends org.apache.commons.beanutils.Converter {}

Teraz metoda rejestrująca takiego konwerterka nie musi mieć explicite podanej klasy, pobierze ją sobie z szablonu korzystając z metody getGenericInterfaces:

	public <T> BeanTransformer addConverter(Converter<T> c, boolean skipConverterImpl) {
		Type[] types = c.getClass().getGenericInterfaces();
		Class<T> clz = (Class<T>) ((ParameterizedType) types[0]).getActualTypeArguments()[0];
		if (!skipConverterImpl) {
			c = new ConverterImpl(clz, c);
		}
		converters.put(clz, c);
		return this;
	}

Jeszcze tylko pozostaje wyjaśnić znaczenie tajemniczej klasy ConverterImp – ona to mianowicie jest sposobem na wydzielenie części wspólnej funkcjonalności dla wszystkich converter’ów a jednocześnie nie wymusza na użytkowniku jej znajomości, wiec po prostu dekoruje metodę convert i dopiero dopuszcza do głosu konkretna implementację, jeżeli wspólna implementacja nie wie jak dany obiekt utworzyć, no ale dośc gadania:

	private static class ConverterImpl<T> implements Converter<T> {
		private Class<T> self;
		private Converter<T> target;
		public ConverterImpl(Class<T> self, Converter<T> target) {
			this.self = self;
			this.target = target;
		}
		@SuppressWarnings("unchecked")
		public Object convert(Class arg0, Object arg1) {
/*
			corrected after comment by bob
			if (self.isAssignableFrom(arg1.getClass())) {
*/
			if (self.isInstance(arg1)) {
				return arg1;
			}
			return target.convert(arg0, arg1);
		}
	}

Mając convertery możemy je sobie włączać i wyłączac przed i po konwersji:

	private void registerConverters() {
		for (Class<?> clz : converters.keySet()) {
			ConvertUtils.register(converters.get(clz), clz);
		}
	}

	private void deregisterConverters() {
		for (Class<?> clz : converters.keySet()) {
			ConvertUtils.deregister(clz);
		}
	}

// w metodzie transform dodajemy wywołania:
			registerConverters();
			BeanUtils.populate(toBean, properties);
			deregisterConverters();

Dla tych, których zaciekawiłem tym wpisem wrzuciłem projekt (wraz z unitami!) na githuba.

Wiem, wiem o tym jak dodać paczkę debianową oracle 10g xe, jest aż nadto. Instrukcja instalacji rozszerzenia PDO_OCI też nie jest skomplikowana (Ci którzy instalowali mysql’a z paczki repozytoriów musza usunąć PDO mysql’owe i dodac to dostarczane przez pecl’a). Wpisujemy php -m i dostajemy PDO_OCI na liście zainstalowanych modułów, odpalamy skrypcik z wiersza poleceń i wyglada na to, że wszystko jest cacy…

…problem pojawia się jak chcemy ten sam skrypt odpalić za pośrednictwem apache’a

Mnie Xdebug nagrodził błędem:

Unable to open PDO connection [wrapped: SQLSTATE[]: pdo_oci_handle_factory: OCI_INVALID_HANDLE (/tmp/pear/temp/PDO_OCI-1.0/oci_driver.c:463)]

Pierwszy strzał to dodanie zmiennych środowiskowych ORA*, no ale to nie pomogło, później to samo dla użytkownika www-data, też nic, aż w końcu wyguglałem, że trzeba zmienne środowiskowe dopisac do pliku konfiguracyjengo apache’a (ustawianie ich w trakcie działania nic nie daje), no więc poniżej moja konfiguracja:

marek2@marek-laptop:~$ cat /etc/apache2/envvars
# envvars – default environment variables for apache2ctl# Since there is no sane way to get the parsed apache2 config in scripts, some
# settings are defined via environment variables and then used in apache2ctl,
# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.
export ORACLE_HOME=/usr/lib/oracle/xe/app/oracle/product/10.2.0/server
export ORACLE_SID=XE
export PATH=$ORACLE_HOME/bin:$PATH
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$LD_LIBRARY_PATH
export ORACLE_OWNER=oraclexeexport APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
export APACHE_PID_FILE=/var/run/apache2.pid