Witam w drugiej części przewodnika “Vaadin jako klient webservice”. Dzisiaj zajmiemy się kilkoma sprawami natury „ogólnej”. Stworzymy szablon UI naszej aplikacji. Własny komponent za pomocą layotów i własnych stylów CSS oraz prosty komponent, który będzie wykorzystywał technologię Server Push (Comet).

Dla osób niecierpliwych. Ten cykl ma na celu nie tylko pokazanie jak zintegrować Vaadin z WS, ale też jak wygląda cały cykl tworzenia trochę bardziej skomplikowanej aplikacji. Zatem nie marudźcie tylko czytajcie.

Pomysł na GUI

Vaadin prawie nie ogranicza nas w zakresie tworzenia GUI. Jeżeli czegoś nie można zrobić za pomocą standardowych komponentów to zawsze możemy zejść poziom niżej i użyć GWT albo narysować sobie komponent jako obrazek i następnie dodać mu jakieś właściwości. Możemy też napisać własny szablon (o tym za chwilę). Na nic to się zda jeżeli nie będziemy wiedzieli co chcemy uzyskać jako efekt końcowy.
Pomysł jest taki, żeby w każda metoda WS miała własne okienko gdzie będzie można sobie przeglądać wyniki i ją wywoływać. Dodatkowo stworzymy sobie, w celach demonstracyjnych, komponent używający ICEPuch, a stopkę zrobimy za pomocą szablonu.
Jazda.

Szablony

Omawiając tworzenie własnych komponentów Vaadin pominąłem szablony. Uznałem je za mało przydatne rozwiązanie, niewarte uwagi. Ostatnio jednak tworząc pewną aplikację przekonałem się do tego rozwiązania. Jest lekkie i praktyczne. Na początek tworzymy własny wygląd aplikacji. Pisałem o tym kiedyś, ale dla przypomnienia. W katalogu src/main/webapp/ tworzymy katalog VAADIN/themes/codecopiler/layouts/ i plik VAADIN/themes/codecopiler/styles.css o następującej treści:

Listing 1. Arkusz CSS naszej aplikacji

@import "../reindeer/styles.css";

W kolejnym kroku w katalogu layouts tworzymy plik footer.html:

Listing 2. Szablon stopki

<div class="cc-footer">YourProduct uses <a href="http://ideone.com">ideone.com</a>
© by <a href="http://sphere-research.com">Sphere Research Labs</a></div>

Wszystko. Na koniec trzeba jeszcze dodać do spring.xml konfigurację naszego komponentu:

Listing 3. Szablon stopki jako bean

<bean class="com.vaadin.ui.CustomLayout" id="footer"><constructor-arg value="footer"></constructor-arg></bean>

Później w aplikacji będzie można pobrać komponent za pomocą odpowiedniego helpera.

Metody na zakładkach

Kolejnym krokiem jest stworzenie głównego layoutu. Będzie opierał się o system zakładek. Nie będziemy kombinować z jakimś upiększaniem…

Listing 3. Szablon stopki jako bean

<bean class="com.vaadin.ui.TabSheet" id="appTabs" scope="prototype"></bean>

Dla mniej spostrzegawczych. O ile stopka jest singletonem to już zakładki nim nie są. Jeżeli by były to by wszyscy użyszkodnicy mieli wyświetlane to samo w tym samym czasie. Głupio…

Prosty komponent ICEPush

Na koniec zabawy z komponentami stworzymy prosty komponent wykorzystujący ICEPush. Będzie on zawierał informacje o stanie serwera.
Zaczniemy od stworzenie prostego interfejsu dla uniwersalnej sondy.

Listing 4. StateProbe<T> operuje na obiekcie stanu T

package pl.koziolekweb.vaadin.probes;

public interface StateProbe<T> {

	public void addStateObserver(StateObserver<T> stateObserver);

	public T getState();

	public void startSampling();

	public void stopSampling();

}

Informacje jakich potrzebujemy to ilość procesorów ilość dostępnego RAM, całkowita ilość RAM przydzielonego JVM i całkowita ilość dostępnej pamięci RAM.
W tym celu stworzymy sobie jeszcze jeden interfejs. SystemProbe, który będzie zwracał potrzebne informacje. Można by użyć klasy Runtime wprost, ale raz, że nie będziemy wtedy mieli możliwości napisania sensownych testów, a dwa zbyt mocno zwiążemy się, z konkretną implementacją.

Listing 5. SystemProbe pozwala na pobranie informacji o systemie

package pl.koziolekweb.vaadin.probes.system;

public interface SystemProbe {

	long getMaxMemory();

	long getTotalMemory();

	long getFreeMemory();

	long getAvailableProcessors();

	String getSystemInfo();
}

W bardziej rozbudowanych systemach można obudować ten interfejs w taki sposób by pobierać dane o wszystkich maszynach w klastrze 😀 No ale my tu gadu gadu, a czas leci.
Implementacja interfejsu jest prosta jak konstrukcja cepa:

Listing 6. Implementacja SystemProbe

package pl.koziolekweb.vaadin.probes.system;

public class SystemProbeJVMRuntimeImpl implements SystemProbe {

	private Runtime runtime;

	public SystemProbeJVMRuntimeImpl() {
		runtime = Runtime.getRuntime();
	}

	@Override
	public long getMaxMemory() {
		return runtime.maxMemory();
	}

	@Override
	public long getTotalMemory() {
		return runtime.totalMemory();
	}

	@Override
	public long getFreeMemory() {
		return runtime.freeMemory();
	}

	@Override
	public long getAvailableProcessors() {
		return runtime.availableProcessors();
	}

	@Override
	public String getSystemInfo() {
		return System.getProperty("os.name") + " " + System.getProperty("os.arch") + " "
				+ System.getProperty("os.version");
	}
}

Tutaj uwaga. Nie testujemy tej klasy. Nie ma to w tym przypadku sensu. Nie możemy kontrolować co zwróci nam system, zatem nie możemy napisać sensownego testu.
W kolejnym kroku implementujemy wspomniany wcześniej interfejs StateProbe<T>.

Listing 7. Implementacja StateProbe<T>

package pl.koziolekweb.vaadin.probes.system;

import java.util.Collection;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.koziolekweb.vaadin.probes.StateObserver;
import pl.koziolekweb.vaadin.probes.StateProbe;

public class ServerStateProbe implements StateProbe<State> {

	private class Probe implements Runnable {

		private boolean listen;

		private SystemProbe runtime;

		public Probe(SystemProbe runtime) {
			this.runtime = runtime;
			listen = false;
		}

		@Override
		public void run() {
			while (listen) {
				inform(readState());
				sleep();
			}
		}

		public void start() {
			listen = true;
			Thread thread = new Thread(this);
			thread.start();
		}

		public void stop() {
			listen = false;
		}

		private State readState() {
			return new State(runtime.getAvailableProcessors(), runtime.getTotalMemory(), runtime.getFreeMemory(),
					runtime.getMaxMemory(), runtime.getSystemInfo());
		}

		@SuppressWarnings("static-access")
		private void sleep() {
			try {
				Thread.currentThread().sleep(1000);
			} catch (InterruptedException e) {
			}
		}

	}

	private final Probe probe;

	private Collection<StateObserver<State>> observers;

	private Logger logger;

	private State state;

	public ServerStateProbe(SystemProbe runtime) {
		this.probe = new Probe(runtime);
		logger = LoggerFactory.getLogger("root");
	}

	@Override
	public void addStateObserver(StateObserver<State> stateObserver) {
		observers.add(stateObserver);
	}

	@Override
	public State getState() {
		return state;
	}

	public void setLogger(Logger logger) {
		this.logger = logger;
	}

	public void setObservers(Collection<StateObserver<State>> observers) {
		this.observers = observers;
	}

	@Override
	@PostConstruct
	public void startSampling() {
		logger.info("Starting server state probe service ...");
		probe.start();
		logger.info("Starting server state probe service ... DONE");
	}

	@Override
	@PreDestroy
	public void stopSampling() {
		logger.info("Stoping server state probe service ...");
		probe.stop();
		logger.info("Stoping server state probe service ... DONE");
	}

	private void inform(State state) {
		this.state = state;
		for (StateObserver<State> observer : observers) {
			observer.update(state);
		}
	}
}

Oraz sama klasa SystemState:

Listing 8. SystemState

package pl.koziolekweb.vaadin.probes.system;

public class SystemState {

	private final long availableProcessors;

	private final long totalMemory;

	private final long freeMemory;

	private final long maxMemory;

	private final String systemInfo;

	public SystemState(long availableProcessors, long totalMemory, long freeMemory, long maxMemory, String systemInfo) {
		super();
		this.availableProcessors = availableProcessors;
		this.totalMemory = totalMemory;
		this.freeMemory = freeMemory;
		this.maxMemory = maxMemory;
		this.systemInfo = systemInfo;
	}

	public long getAvailableProcessors() {
		return availableProcessors;
	}

	public long getFreeMemory() {
		return freeMemory;
	}

	public long getTotalMemory() {
		return totalMemory;
	}

	public long getMaxMemory() {
		return maxMemory;
	}

}

Na koniec przydałby się jeszcze komponent Vaadin, który będzie to wszystko wyświetlał. Musi on implementować interfejs StateObserver<T>

Listing 9. StateObserver<T>

package pl.koziolekweb.vaadin.probes;

public interface StateObserver<T> {

	void update(T t);
}

Który jest w praktyce interfejsem obserwatora (patrz metoda inform w ServerStateProbe).
Sam komponent wygląda tak:

Listing 10. ServerStateComponent

package pl.koziolekweb.vaadin.codecompiler.gui;

import org.vaadin.artur.icepush.ICEPush;

import pl.koziolekweb.vaadin.probes.StateObserver;
import pl.koziolekweb.vaadin.probes.system.SystemState;

import com.vaadin.ui.Component;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;

@SuppressWarnings("serial")
public class ServerStateComponent extends CustomComponent implements StateObserver<SystemState> {

	private HorizontalLayout mainLayout;
	private Label cpuValue;
	private Label freeMemValue;
	private Label totalMemValue;
	private Label maxMemValue;

	private ICEPush icePush;

	public ServerStateComponent() {
		buildLayout();
		setCompositionRoot(mainLayout);
	}

	@Override
	public void addComponent(Component c) {
		mainLayout.addComponent(c);
	}

	public void setIcePush(ICEPush icePush) {
		mainLayout.addComponent(icePush);
		this.icePush = icePush;
	}

	@Override
	public void update(SystemState state) {
		setNewState(state);
		pushClient();
	}

	private void buildCpu() {
		cpuValue = new Label();
		cpuValue.setCaption("Av CPU:");
		cpuValue.setWidth("100px");
		mainLayout.addComponent(cpuValue);
	}

	private void buildFreeMem() {
		freeMemValue = new Label();
		freeMemValue.setCaption("Free Memory:");
		freeMemValue.setWidth("100px");
		mainLayout.addComponent(freeMemValue);
	}

	private void buildLayout() {
		mainLayout = new HorizontalLayout();
		buildCpu();
		buildFreeMem();
		buildTotalMem();
		buildMaxMem();
	}

	private void buildTotalMem() {
		totalMemValue = new Label();
		totalMemValue.setCaption("Total Memory:");
		totalMemValue.setWidth("100px");
		mainLayout.addComponent(totalMemValue);
	}

	private void buildMaxMem() {
		maxMemValue = new Label();
		maxMemValue.setCaption("Maximum Memory:");
		maxMemValue.setWidth("100px");
		mainLayout.addComponent(maxMemValue);
	}

	private void pushClient() {
		requestRepaintAll();
		if (icePush.getApplication() != null) {
			icePush.push();
		}
	}

	private void setNewState(SystemState t) {
		cpuValue.setValue(t.getAvailableProcessors());
		freeMemValue.setValue(t.getFreeMemory());
		totalMemValue.setValue(t.getTotalMemory());
		maxMemValue.setValue(t.getMaxMemory());
	}
}

Dla nas najważniejsze jest to co dzieje się w metodzie update(). Najpierw ustawiany jest nowy stan komponentu, a następnie wywoływana metoda push(). Działa ona mniej więcej w ten sposób, że najpierw oznaczamy po stronie Vaadin, że komponent należy odświerzyć requestRepaintAll(), a następnie wysyłamy informację do przeglądarki. Rzecz w tym, że serwer nie może w normalnych warunkach wysłać takiej informacji. Zawdzięczamy to protokołowi http, który jest bezstanowy. By uzyskać taki efekt należy otworzyć połączenie „ciągłe” i na bierząco wysyłać do przeglądarki informacje w czymś w rodzaju streamingu. Technologia ta o wdzięcznej nazwie Comet jest stosunkowo świeża i dopiero zaczyna się rozwijać. Wynika to z dwóch rzeczy. Raz utrzymanie połączenia, nawet o niewielkiej ilości przesyłanych danych, mocno obciąża zarówno serwer jak i klienta. Pokombinujcie z czasem spania wątku w klasie Probe. Zobaczycie o co chodzi. Dwa wcześniej nie było takich wymagań. Korzystam z ICEPush, ale jak już pojawi się Vaadin 7 to będzie można obejść się bez niego.
Na koniec konfiguracja spring.xml

Listing 11. Nowości w spring.xml

<bean class="pl.koziolekweb.vaadin.probes.system.SystemProbeJVMRuntimeImpl" id="systemProbe"></bean><bean class="pl.koziolekweb.vaadin.probes.system.ServerStateProbe" id="serverStateListener"><constructor-arg ref="systemProbe"></constructor-arg><property name="observers"><list><ref bean="serverStateComponent"></ref></list></property></bean><bean class="org.vaadin.artur.icepush.ICEPush" id="pushUpService" scope="prototype"></bean><bean class="com.vaadin.ui.CustomLayout" id="footer"><constructor-arg value="footer"></constructor-arg></bean><bean class="com.vaadin.ui.TabSheet" id="appTabs" scope="prototype"></bean>

Jutro usługa testowa. Już będziemy pisali klienta obiecuję.