19 czerwca 2008

JBoss Seam, Apache Geronimo z Eclipse Ganymede i Apache Maven 2

Postanowiłem trochę popróbować się z JBoss Seam 2.0.3.CR1 w trochę odmienny sposób niż z seam-gen. Nie ukrywam, że kiedy zobaczyłem te wszystkie katalogi po zaimportowaniu do Eclipse Ganymede trochę się przestraszyłem skomplikowanej struktury katalogowej aplikacji. Niby Seam miał mi ją odchudzić, a tych katalogów i plików jakby więcej. I jeszcze ta czerwona ikonka oznaczająca błąd w projekcie (zmienna nie jest rozwiązywana i w zasadzie błąd jest nieistotny).

Zabrałem się za utworzenie aplikacji seamowej od zera z użyciem Eclipse JEE Ganymede RC3 (i właśnie jak pisałem wersję okazało się, że pojawiła się już kolejna wersja RC4! Nie zasypują gruszek w popiele). Celem było określenie kroków, jakie konieczne są, aby zestawić projekt seamowy w Eclipse.

Zacząłem od Ctrl+N, Dynamic Web Project, podałem nazwę projektu (Project name), przypisałem serwer (Target Runtime) i Finish. Potrzebowałem jeszcze zdefiniować zależności seamowe w projekcie. Java > Build Path > User Libraries, przycisk New..., podałem nazwę JBoss Seam, następnie Add JARs. Kiedy zacząłem dodawać wszystkie zależności pomyślałem sobie, że prościej byłoby po prostu zadeklarować w projekcie pojedyńczą bibliotekę, której dalsze zależności byłyby pobrane automatycznie, za mnie. Do głowy przyszedł Apache Maven 2.

I znowu od zera. Na wzór myfaces-trinidad-tree z EJB 3.0 Redbook opublikowany i doświadczenia z trinidadowym tr:tree postanowiłem stworzyć aplikację seamową, która realizowałaby podobną funkcjonalność - prezentowanie danych z bazy danych w strukturze drzewiastej jak lista katalogów czy podobnie.
 mvn archetype:create \
-DarchetypeArtifactId=maven-archetype-webapp \
-DgroupId=pl.jaceklaskowski.seam \
-DartifactId=seam-richfaces-tree
i mam aplikację. Import do Eclipse (z zainstalowaną wtyczką m2eclipse) - File > Import > Maven Projects, gdzie wskazuję na katalog z właśnie utworzoną aplikacją (C:\projs\sandbox\seam-richfaces-tree).

Pozostaje zadeklarować zależności seamowe w pom.xml i do można tworzyć aplikację. Dobrym projektem wzorcowym, gdzie potrzebne zależności są przejrzyście zadeklarowane wraz z ich wersjami i repozytoriami to po prostu plik root-2.0.3.CR1.pom, który jest POMem dla projektu JBoss Seam. Trochę informacji można znaleźć w zdeaktualizowanym dokumencie Seam published to Maven. Podglądając Java Build Path nowozaimportowanego projektu w Eclipse można dowiedzieć się o minimalnych wymaganiach Seama odnośnie zależności zewnętrznych - Build Path > Configure Build Path > Libraries > Maven Dependencies (widok po kilku zmianach, o których za chwilę).

Pobieżna lektura 1.2. Your first Seam application: the registration example, gdzie dowiaduję się o koniecznych zmianach w deskryptorze web.xml związanych z rejestracją słuchaczy (listeners) inicjujących niezbędne elementy aplikacji seamowej.

Pierwsze zaskoczenie spoza obszaru Seama to edytor deskryptora web.xml, ale sądzę, że to zasługa zbioru wtyczek JBoss Tools niż samego Eclipse Ganymede (może ktoś to potwierdzić?).

Podnoszę wersję aplikacji webowej do 2.5 (z 1.3 domyślnie deklarowanej przez archetyp maven-archetype-webapp - na zrzucie powyżej już po poprawce). W zasadzie to kopiuję zawartość Example 1.5. z dokumentacji Seama.

Wykonuję mvn package dla pewności, że wszystko poprawnie zestawione (chociaż wtyczka m2eclipse w Eclipse robi dokładnie to samo po każdej zmianie, to mimo wszystko jakiś taki nieufny dzisiaj jestem i ciągnie mnie do linii poleceń - nawet mój syn dzisiaj zauważył, że wciąż mam otwartego Cygwina).
 <?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<listener>
<listener-class>org.jboss.seam.servlet.SeamListener</listener-class>
</listener>
<listener>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.seam</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
</web-app>
Każdorazowe wywołanie adresu z rozszerzeniem seam spowoduje wzbudzenie servletu facesowego (czytaj: fejsowego) - Faces Servlet, który zmodyfikuje adres na stronę o rozszerzeniu xhtml, która potencjalnie będzie wykorzystywała Facelets jako konstrukcji dla stron seamowych. Oczywiście strona xhtml nie musi być pisana z użyciem facelets - może to być po prostu zwykła strona xhtml. Sprawdzam.

Zaznaczając katalog src/main/webapp, spod prawego klawisza myszy wybieram New > Other... (Ctrl+N), a dalej Web > HTML, przycisk Next, podaję nazwę nowego pliku witaj.xhtml, ponownie Next, gdzie wybieram New XHTML File (1.0 strict) - nigdy nie wiem, jaka jest różnica między 1.0 strict a transitional i kończę przyciskiem Finish. Dodaję Witaj nieznajomy... do strony.
 <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Witaj Nieznajomy!</title>
</head>
<body>
Witaj Nieznajomy!
</body>
</html>
i w końcu nadeszła pora spróbować uruchomić aplikację semi-seamową (semi, gdyż jedynym łącznikiem między aplikacją a Seamem jest zadeklarowanie łączenia w deskryptorze web.xml w postaci listener-class jako org.jboss.seam.servlet.SeamListener).

Zastanawiam się jak teraz uruchomić aplikację z poziomu Eclipse?! Czyżby pozostawało mi jedynie skorzystać z mvn clean jetty:run?
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ mvn clean jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ------------------------------------------------------------------------
[INFO] The plugin 'org.apache.maven.plugins:maven-jetty-plugin' does not exist or no valid version could be found
[INFO] ------------------------------------------------------------------------
[INFO] For more information, run Maven with the -e switch
[INFO] ------------------------------------------------------------------------
[INFO] Total time: < 1 second
No tak, brakuje konfiguracji wtyczki jetty w projekcie. Po konfiguracji wtyczki (więcej w Konfiguracja wtyczki maven-jetty-plugin w artykule Deklaratywne bezpieczeństwo w JSF z JAAS i Jetty) uruchomienie kończy się błędem (!)
 [INFO] Starting jetty 6.1.10 ...
2008-06-18 22:44:23.549::INFO: jetty-6.1.10
2008-06-18 22:44:23.705::WARN: Could not instantiate listener com.sun.faces.config.ConfigureListener
java.lang.ClassNotFoundException: com.sun.faces.config.ConfigureListener
at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at org.codehaus.classworlds.RealmClassLoader.loadClassDirect(RealmClassLoader.java:195)
at org.codehaus.classworlds.DefaultClassRealm.loadClass(DefaultClassRealm.java:255)
at org.codehaus.classworlds.DefaultClassRealm.loadClass(DefaultClassRealm.java:274)
at org.codehaus.classworlds.RealmClassLoader.loadClass(RealmClassLoader.java:214)
at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
at org.mortbay.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:375)
Brakuje bibliotek JSF w aplikacji webowej. Ponownie Java Build Path > Add Library > JSF Libraries, a tam JavaServer Faces Libraries can only be configured for Dynamic Web projects with the JSF faces installed. I tu decyzja, która zajęła mnie już do końca dnia - spróbuję uruchomić aplikację z poziomu mavena z...Apache Geronimo 2.1.1 z wbudowanym Jetty (alternatywnie można skorzystać z Apache Geronimo dystrybuowanym z Apache Tomcat). Powód? Przynajmniej dwa - po pierwsze konfigurację bibliotek JSF mam wtedy z głowy, gdyż są one obowiązkowym elementem każdego serwera Java EE 5 i po drugie nigdy tego jeszcze nie przedstawiałem, więc najwyższa pora. Serwer aplikacyjny Java EE 5, jakim jest Apache Geronimo jest zobligowany wymaganiem specyfikacji Java EE 5 do dostarczania odpowiednich bibliotek JSF bez konieczności nakładania dodatkowego obowiązku troszczenia się o to przez same aplikacje webowe.

Konfiguracja pom.xml z wykorzystaniem wtyczki geronimo-maven-plugin i tym razem mvn integration-test do uruchomienia aplikacji z Geronimo. Dokumentacja wtyczki geronimo-maven-plugin jest trochę nieświeża, więc wymaga uaktualnienia, co załatwiam krótko - mam pobrane źródła wtyczki, wykonuję mvn site w buildsupport/geronimo-maven-plugin i cieszę się świeżutką dokumentacją w buildsupport/geronimo-maven-plugin/target/site. Ostatecznie plik pom.xml prezentuje się następująco:
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.jaceklaskowski.seam</groupId>
<artifactId>seam-richfaces-tree</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>seam-richfaces-tree Maven Webapp</name>
<url>http://www.jaceklaskowski.pl</url>
<repositories>
<repository>
<id>repository.jboss.org</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.org/maven2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.seam</groupId>
<artifactId>jboss-seam</artifactId>
<version>2.0.3.CR1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>seam-richfaces-tree</finalName>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.geronimo.buildsupport</groupId>
<artifactId>geronimo-maven-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<assemblies>
<assembly>
<id>geronimo-jetty</id>
<groupId>org.apache.geronimo.assemblies</groupId>
<artifactId>geronimo-jetty6-javaee5</artifactId>
<version>2.1.1</version>
<classifier>bin</classifier>
<type>zip</type>
</assembly>
<assembly>
<id>geronimo-tomcat</id>
<groupId>org.apache.geronimo.assemblies</groupId>
<artifactId>geronimo-tomcat6-javaee5</artifactId>
<version>2.1.1</version>
<classifier>bin</classifier>
<type>zip</type>
</assembly>
</assemblies>
<defaultAssemblyId>geronimo-jetty</defaultAssemblyId>
<optionSets>
<optionSet>
<id>default</id>
<options>
<option>-XX:MaxPermSize=128m</option>
</options>
</optionSet>
<optionSet>
<id>morememory</id>
<options>
<option>-Xmx512m</option>
<option>-XX:MaxPermSize=128m</option>
</options>
</optionSet>
</optionSets>
</configuration>
<executions>
<execution>
<id>start-server</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start-server</goal>
</goals>
<configuration>
<assemblyId>geronimo-jetty</assemblyId>
<logOutput>true</logOutput>
<background>true</background>
<verifyTimeout>300</verifyTimeout>
<refresh>true</refresh>
<veryverbose>true</veryverbose>
<options>morememory</options>
</configuration>
</execution>
<execution>
<id>deploy-ear</id>
<phase>pre-integration-test</phase>
<goals>
<goal>deploy-module</goal>
</goals>
<configuration>
<moduleArchive>target/seam-richfaces-tree.war</moduleArchive>
<modulePlan>src/main/resources/geronimo-web.xml</modulePlan>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Uruchomienie mvn clean intergration-test, które trochę potrwa zanim Geronimo znajdzie się na dysku, więc warto pomyśleć o przerwie 10-minutowej, w trakcie której zostanie pobrany z Sieci (w końcu to ~76MB).

Po chwili...

Na razie wszystko działa, jeśli chodzi o wystartowanie serwera z aplikacją.

Czas na utworzenie planu dla Geronimo, gdzie nadamy nazwę modułowi dla łatwiejszej jej administracji. Należy rozróżnić pojęcia war od modułu, gdzie moduł jest bytem pierwszej kategorii w Geronimo (reprezentowanym przez zestaw GBeanów), gdzie każdy inny typ aplikacji, jak war, ear, ejb-jar jest jedynie bardziej specjalizowanym modułem. Geronimo rozpoznaje byty, którymi zarządza jedynie poprzez zestaw GBeans i tylko to dla niego się liczy. Samo określenie zestawu GBeans jako war, ear czy podobnie to wymaganie wybranej specyfikacji, w tym przypadku Java EE, którą Geronimo realizuję. Możemy sobie wyobrazić sytuację, że Geronimo będzie realizowało inną specyfikację i jej realizacja również będzie oparta o GBeans. Plik plan jest dokumentem konfiguracyjnym modułu, gdzie konfigurujemy środowisko aplikacji łącznie z mapowaniem logicznych nazw w aplikacji na ich serwerowe zasoby.
 <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://geronimo.apache.org/xml/ns/j2ee/web-2.0.1">
<environment xmlns="http://geronimo.apache.org/xml/ns/deployment-1.2">
<moduleId>
<groupId>pl.jaceklaskowski.seam</groupId>
<artifactId>seam-richfaces-tree</artifactId>
<version>1.0</version>
<type>war</type>
</moduleId>
</environment>
<context-root>/seam-richfaces-tree</context-root>
</web-app>
Możnaby skorzystać z dobrodziejstw Mavena do dynamicznej podmiany wartości moduleId podczas budowania aplikacji, aby nie kopiować ich i tym samym utrzymywać w dwóch miejscach - niektóre z nich mogą się zmienić podczas rozwoju aplikacji, jak numer wersji aplikacji. Pozostawiam temat jako zadanie domowe dla ochotnika ;-)

Po chwili konfiguracji, uruchamiania, itp. uruchomienie aplikacji kończy się...wyjątkowo.
 Deployer operation failed: AbstractWebModuleBuilder: 
Could not load listener class: com.sun.faces.config.ConfigureListener
org.apache.geronimo.common.DeploymentException: AbstractWebModuleBuilder:
Could not load listener class: com.sun.faces.config.ConfigureListener
at org.apache.geronimo.web25.deployment.AbstractWebModuleBuilder.createWebAppClassFinder(AbstractWebModuleBuilder.java:791)
at org.apache.geronimo.web25.deployment.AbstractWebModuleBuilder.createWebAppClassFinder(AbstractWebModuleBuilder.java:759)
at org.apache.geronimo.web25.deployment.AbstractWebModuleBuilder.configureBasicWebModuleAttributes(AbstractWebModuleBuilder.java:836)
at org.apache.geronimo.jetty6.deployment.JettyModuleBuilder.addGBeans(JettyModuleBuilder.java:365)
at org.apache.geronimo.j2ee.deployment.SwitchingModuleBuilder.addGBeans(SwitchingModuleBuilder.java:165)
at org.apache.geronimo.j2ee.deployment.EARConfigBuilder.buildConfiguration(EARConfigBuilder.java:647)
at org.apache.geronimo.deployment.Deployer.deploy(Deployer.java:254)
at org.apache.geronimo.deployment.Deployer.deploy(Deployer.java:133)
Wynika to z faktu, że domyślną implementacją specyfikacji JavaServer Faces (na której oparty jest Seam) jest implementacja referencyjna Suna - JSF RI. Pozostaje "zdjąć" wymaganie na com.sun.faces.config.ConfigureListener w deskryptorze (czym mniej zależności od niestandardowych klas i interfejsów tym lepiej dla kondycji naszej aplikacji, nieprawdaż?). Ponowne uruchomienie i...
 [INFO] Starting modules...
[INFO] Starting module: pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
org.apache.geronimo.kernel.config.LifecycleException:
start of pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war failed
at org.apache.geronimo.kernel.config.SimpleConfigurationManager.startConfiguration(SimpleConfigurationManager.java:566)
at org.apache.geronimo.kernel.config.SimpleConfigurationManager.startConfiguration(SimpleConfigurationManager.java:530)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Caused by: org.apache.geronimo.gbean.InvalidConfigurationException:
Configuration pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war failed to start
due to the following reasons:
The service J2EEApplication=null,WebModule=pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war,
j2eeType=Servlet,name=Faces Servlet did not start because javax.servlet.ServletException:
java.lang.IllegalStateException: No Factories configured for this Application.
This happens if the faces-initialization does not work at all -
make sure that you properly include all configuration settings necessary for a basic faces application
and that all the necessary libs are included.
Also check the logging output of your web application and your container for any exceptions!
If you did that and find nothing, the mistake might be due to the fact that you use some special
web-containers which do not support registering context-listeners via TLD files and a context listener
is not setup in your web.xml.
A typical config looks like this;
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
Nie jest jednak tak wspaniale z tą niezależnością technologiczną, jak mogłoby się wydawać. Pora zatem uzależnić aplikację od Apache MyFaces i dopisać odpowiednią sekcję do deskryptora wdrożenia web.xml zgodnie z treścią komunikatu.
 <?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<listener>
<listener-class>org.jboss.seam.servlet.SeamListener</listener-class>
</listener>
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.seam</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
</web-app>
I tak mógłbym cały czas próbować uruchamiać aplikację bazując na błędach, które są zgłaszane przez wtyczkę mavenową dla Geronimo, gdzie wciąż widniał komunikat o potencjalnym braku konfiguracji <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>, aż po bodajże 10-tym razie, bez pomysłów i prawie całkowicie zrezygnowany (wyjątkowo szybko się zniechęcam), spojrzałem do dziennika zdarzeń (log) - target/geronimo-logs/org.apache.geronimo.mavenplugins.geronimo.server.StartServerMojo.log, którego utworzenie nakazałem przez konfigurację parametru logOutput (z wartością true) w pom.xml, a tam:
 Geronimo startup complete
10:34:01,031 INFO [ServletContextListener] Welcome to Seam 2.0.3.CR1
...
10:34:23,265 ERROR [log] Failed startup of context
org.apache.geronimo.jetty6.handler.TwistyWebAppContext@71eb24{/seam-richfaces-tree,
file:/C:/projs/sandbox/seam-richfaces-tree/target/geronimo-jetty6-javaee5-2.1.1/
repository/pl/jaceklaskowski/seam/seam-richfaces-tree/1.0/seam-richfaces-tree-1.0.war/}
java.lang.RuntimeException: Could not create Component: org.jboss.seam.core.validators
at org.jboss.seam.init.Initialization.addComponent(Initialization.java:989)
at org.jboss.seam.init.Initialization.installComponents(Initialization.java:911)
at org.jboss.seam.init.Initialization.init(Initialization.java:589)
at org.jboss.seam.servlet.SeamListener.contextInitialized(SeamListener.java:34)
at org.mortbay.jetty.handler.ContextHandler.startContext(ContextHandler.java:540)
at org.mortbay.jetty.servlet.Context.startContext(Context.java:135)
at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1220)
at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:510)
at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
at org.apache.geronimo.jetty6.JettyWebAppContext$StartCommand.lifecycleMethod(JettyWebAppContext.java:366)
...
Caused by: java.lang.NoClassDefFoundError: [Lorg/hibernate/validator/InvalidValue;
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2395)
at java.lang.Class.privateGetPublicMethods(Class.java:2519)
at java.lang.Class.getMethods(Class.java:1406)
at org.jboss.seam.Component.hasAnnotation(Component.java:1070)
at org.jboss.seam.Component.(Component.java:229)
at org.jboss.seam.Component.(Component.java:217)
at org.jboss.seam.init.Initialization.addComponent(Initialization.java:974)
... 76 more
Wniosek jeden, brakuje zależności aplikacji (bibliotek pomocnicych), które świadomie początkowo zawęziłem wyłącznie do org.jboss.seam.jboss-seam (sekcja dependencies w pom.xml). Teraz zaczyna się żmudna praca analityka cudzej pracy i odgadywania zależności projektu JBoss Seam. Najłatwiej byłoby po prostu dodać wszystkie seamowe zależności opisane w dokumencie Seam published to Maven w sekcji Using Seam with Maven, ale ja postanowiłem zawęzić konfigurację zależności do niezbędnego minimum. Analizując root-2.0.3.CR1.pom odnajduję wskazanie na brakującą zależność Hibernate Validator.
 <dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>3.0.0.GA</version>
</dependency>
Po tej zmianie w pom.xml uruchomienie mvn clean pre-integration-test kończy się z BUILD SUCCESSFUL.
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ mvn clean pre-integration-test
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building seam-richfaces-tree Maven Webapp
[INFO] task-segment: [clean, pre-integration-test]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory c:\projs\sandbox\seam-richfaces-tree\target
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] No sources to compile
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] No sources to compile
[INFO] [surefire:test]
[INFO] No tests to run.
[INFO] [war:war]
[INFO] Packaging webapp
[INFO] Assembling webapp[seam-richfaces-tree] in [c:\projs\sandbox\seam-richfaces-tree\target\seam-richfaces-tree]
[INFO] Processing war project
[INFO] Webapp assembled in[234 msecs]
[INFO] Building war: c:\projs\sandbox\seam-richfaces-tree\target\seam-richfaces-tree.war
Downloading: http://download.java.net/maven/1//woodstox/poms/wstx-asl-3.2.1.pom
Downloading: http://people.apache.org/repo/m2-incubating-repository//woodstox/wstx-asl/3.2.1/wstx-asl-3.2.1.pom
Downloading: http://repo1.maven.org/maven2/woodstox/wstx-asl/3.2.1/wstx-asl-3.2.1.pom
Downloading: http://repository.jboss.org/maven2/woodstox/wstx-asl/3.2.1/wstx-asl-3.2.1.pom
Downloading: http://repo1.maven.org/maven2/woodstox/wstx-asl/3.2.1/wstx-asl-3.2.1.pom
[INFO] [geronimo:start-server {execution: start-server}]
[INFO] Using assembly configuration: geronimo-jetty
[INFO] Using assembly artifact: org.apache.geronimo.assemblies:geronimo-jetty6-javaee5:zip:bin:2.1.1:provided
[INFO] Using geronimoHome: C:\projs\sandbox\seam-richfaces-tree\target\geronimo-jetty6-javaee5-2.1.1
[INFO] Installing assembly...
[INFO] Expanding:
C:\.m2\org\apache\geronimo\assemblies\geronimo-jetty6-javaee5\2.1.1\geronimo-jetty6-javaee5-2.1.1-bin.zip into
C:\projs\sandbox\seam-richfaces-tree\target
[INFO] Starting Geronimo server...
[INFO] Selected option set: morememory
[INFO] Redirecting output to:
c:\projs\sandbox\seam-richfaces-tree\target\geronimo-logs\org.apache.geronimo.mavenplugins.geronimo.server.StartServerMojo.log
[INFO] Waiting for Geronimo server...
log4j:WARN No appenders could be found for logger (org.apache.geronimo.mavenplugins.geronimo.ServerProxy).
log4j:WARN Please initialize the log4j system properly.
[INFO] Geronimo server started in 0:00:32.094
[INFO] [geronimo:deploy-module {execution: deploy-ear}]
[INFO] Using non-artifact based module archive: c:\projs\sandbox\seam-richfaces-tree\target\seam-richfaces-tree.war
[INFO] Using non-artifact based plan: c:\projs\sandbox\seam-richfaces-tree\src\main\resources\geronimo-web.xml
[INFO] Distributing module artifact: c:\projs\sandbox\seam-richfaces-tree\target\seam-richfaces-tree.war with plan
c:\projs\sandbox\seam-richfaces-tree\src\main\resources\geronimo-web.xml
[INFO] Starting modules...
[INFO] Starting module: pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
[INFO] Started module(s):
[INFO] [0] pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 minutes 5 seconds
To lubię. Teraz mogłoby paść pytanie jak sprawdzić działanie aplikacji? Mamy możliwość automatycznego uruchomienia testów za pomocą Selenium albo Twill (książka o nich czeka na lekturę w Bibliotece Warszawskiego JUGa) albo po prostu uruchomienie Geronimo oddzielnie, poza projektem i na nim wykonanie rozmieszczenia aplikacji. Sprawdzam trop.

Uruchamiam Geronimo z ./bin/geronimo.sh run -vv.
 jlaskowski@work /cygdrive/c/geronimo
$ ./bin/geronimo.sh run -vv
Using GERONIMO_BASE: c:\geronimo
Using GERONIMO_HOME: c:\geronimo
Using GERONIMO_TMPDIR: var\temp
Using JRE_HOME: c:\apps\java5\jre
11:27:19,015 DEBUG [BasicKernel] Starting boot
...
a następnie w innej konsoli:
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ c\:/geronimo/bin/deploy.sh -u system -p manager deploy \
target/seam-richfaces-tree.war src/main/resources/geronimo-web.xml
Na konsoli Geronimo pojawiają się wtedy komunikaty:
 11:35:01,859 INFO  [ServletContextListener] Welcome to Seam 2.0.3.CR1
...
a po chwili
 Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
No tak! Przecież w samej dokumentacji Seama w rozdziale Chapter 2. Getting started with Seam, using seam-gen była o tym wzmianka, aby podnieść parametry pamięci dla serwera aplikacyjnego, do uruchomienia Seama, np. -Xms256m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256.

Zatrzymuję serwer Geronimo (zwykłe Ctrl+C wystarczy, chociaż szybciej po prostu kill -9 czy podobnie) i ponownie uruchamiam już z podniesionymi parametrami dotyczącymi pamięci.
 jlaskowski@work /cygdrive/c/geronimo
$ GERONIMO_OPTS="-Xms256m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256" ./bin/geronimo.sh run -vv
Using GERONIMO_BASE: c:\geronimo
Using GERONIMO_HOME: c:\geronimo
Using GERONIMO_TMPDIR: var\temp
Using JRE_HOME: c:\apps\java5\jre
11:41:23,343 DEBUG [BasicKernel] Starting boot
...
Geronimo startup complete
I wykonanie polecenia rozmieszczającego aplikację na Geronimo.
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ c\:/geronimo/bin/deploy.sh -u system -p manager deploy \
target/seam-richfaces-tree.war src/main/resources/geronimo-web.xml
Using GERONIMO_BASE: c:\geronimo
Using GERONIMO_HOME: c:\geronimo
Using GERONIMO_TMPDIR: var\temp
Using JRE_HOME: c:\apps\java5\jre
Error: Unable to distribute seam-richfaces-tree.war:
org.apache.geronimo.kernel.config.ConfigurationAlreadyExistsException:
Configuration already exists:
pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war

Configuration already exists:
pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
Może być konieczne najpierw odinstalowanie modułu (=aplikacji webowej) po wcześniejszej niepomyślnej instalacji zakończonej OOME.
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ c\:/geronimo/bin/deploy.sh -u system -p manager undeploy \
pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
Using GERONIMO_BASE: c:\geronimo
Using GERONIMO_HOME: c:\geronimo
Using GERONIMO_TMPDIR: var\temp
Using JRE_HOME: c:\apps\java5\jre
Module pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
uninstalled.

Undeployed pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war
I podejście finalne, podsumowujące prace - deploy.
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ c\:/geronimo/bin/deploy.sh -u system -p manager deploy \
target/seam-richfaces-tree.war src/main/resources/geronimo-web.xml
Using GERONIMO_BASE: c:\geronimo
Using GERONIMO_HOME: c:\geronimo
Using GERONIMO_TMPDIR: var\temp
Using JRE_HOME: c:\apps\java5\jre
Deployed pl.jaceklaskowski.seam/seam-richfaces-tree/1.0/war @
/seam-richfaces-tree
I jest gites! Sprawdzam jeszcze konsolę Geronimo:
 11:50:12,656 INFO  [ServletContextListener] Welcome to Seam 2.0.3.CR1
...
11:50:32,000 INFO [Initialization] initializing Seam
...
11:50:32,406 INFO [Initialization] Installing components...
...
11:50:32,812 INFO [Contexts] starting up: org.jboss.seam.navigation.pages
11:50:32,890 INFO [Pages] no pages.xml file found: /WEB-INF/pages.xml
11:50:32,890 INFO [Contexts] starting up: org.jboss.seam.security.facesSecurityEvents
11:50:32,890 INFO [Initialization] done initializing Seam
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.PRETTY_HTML' found, using default value true
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.ALLOW_JAVASCRIPT' found, using default value true
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.READONLY_AS_DISABLED_FOR_SELECTS' found, using default value true
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.RENDER_VIEWSTATE_ID' found, using default value true
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.STRICT_XHTML_LINKS' found, using default value true
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.CONFIG_REFRESH_PERIOD' found, using default value 2
11:50:32,921 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.VIEWSTATE_JAVASCRIPT' found, using default value false
11:50:32,921 INFO [MyfacesConfig] Tomahawk jar not available. Autoscrolling, DetectJavascript, AddResourceClass and CheckExtensionsFilter are disabled now.
11:50:32,921 INFO [MyfacesConfig] Starting up Tomahawk on the RI-JSF-Implementation.
11:50:32,921 INFO [MyfacesConfig] Starting up Tomahawk on the MyFaces-JSF-Implementation
11:50:32,921 ERROR [MyfacesConfig] Both MyFaces and the RI are on your classpath. Please make sure to use only one of the two JSF-implementations.
11:50:33,125 INFO [AbstractFacesInitializer] ServletContext
'C:\geronimo\repository\pl\jaceklaskowski\seam\seam-richfaces-tree\1.0\seam-richfaces-tree-1.0.war\' initialized.
Mimo ostatniego błędu można uznać rozmieszczenie aplikacji za pomyślne. Jeszcze tylko zajrzeć przeglądarką pod adres http://localhost:8080/seam-richfaces-tree/.
I koniec. Czyżby? Nie, jeszcze nie! W końcu to jedynie przykładowy index.jsp, który tworzony jest w ramach zestawiania projektu mavenowym archetypem maven-archetype-webapp. Mimo to Seam już się wzbudził, co można zaobserwować na konsoli Geronimo.
 11:53:03,343 INFO  [Contexts] starting up: org.jboss.seam.web.session
11:53:03,343 INFO [Contexts] starting up: org.jboss.seam.security.identity
Aby go jednak na dobre rozruszać musielibyśmy przynajmniej zajrzeć pod adres http://localhost:8080/seam-richfaces-tree/witaj.seam.

Błąd! Jednak zajmę się tym ostatnim komunikatem błędu
 ERROR [MyfacesConfig] Both MyFaces and the RI are on your classpath. 
Please make sure to use only one of the two JSF-implementations.
Tylko skąd on się bierze?! W katalogu WEB-INF/lib nie ma nic co wskazywałoby na obecność JSF RI.
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ ls -l target/seam-richfaces-tree/WEB-INF/lib/
total 5208
-rwx------+ 1 jlaskowski None 443432 Apr 18 13:31 antlr-2.7.6.jar
-rwx------+ 1 jlaskowski None 26361 Jun 7 12:23 asm-1.5.3.jar
-rwx------+ 1 jlaskowski None 16757 Apr 18 13:31 asm-attrs-1.5.3.jar
-rwx------+ 1 jlaskowski None 282338 Jun 7 12:23 cglib-2.1_3.jar
-rwx------+ 1 jlaskowski None 175426 Jun 7 23:44 commons-collections-2.1.1.jar
-rwx------+ 1 jlaskowski None 38015 Apr 18 12:04 commons-logging-1.0.4.jar
-rwx------+ 1 jlaskowski None 303207 Jun 18 22:19 dom4j-1.6.1-jboss.jar
-rwx------+ 1 jlaskowski None 208048 Apr 18 13:30 ehcache-1.2.3.jar
-rwx------+ 1 jlaskowski None 29309 Jun 18 22:19 el-api-1.0.jar
-rwx------+ 1 jlaskowski None 2242529 Jun 19 11:18 hibernate-3.2.4.sp1.jar
-rwx------+ 1 jlaskowski None 60992 Jun 19 11:18 hibernate-validator-3.0.0.GA.jar
-rwx------+ 1 jlaskowski None 459663 Jun 18 22:19 javassist-3.3.GA.jar
-rwx------+ 1 jlaskowski None 133493 Jun 18 22:19 jboss-el-2.0.1.GA.jar
-rwx------+ 1 jlaskowski None 869271 Jun 18 22:19 jboss-seam-2.0.3.CR1.jar
-rwx------+ 1 jlaskowski None 8812 Jun 7 12:23 jta-1.0.1B.jar
Jest kilka jarów, które są niepotrzebne - jta, el-api, commons-logging, commons-collections, cglib, asm* oraz antlr, które są dostępne w Geronimo, ale gdzie jest cokolwiek związanego z JSF RI?! Wykluczam wspomniane jary - nie zaszkodzi, a może przypadkiem pomoże ;-)
 jlaskowski@work /cygdrive/c/projs/sandbox/seam-richfaces-tree
$ ls -l target/seam-richfaces-tree/WEB-INF/lib/
total 4644
-rwx------+ 1 jlaskowski None 443432 Apr 18 13:31 antlr-2.7.6.jar
-rwx------+ 1 jlaskowski None 303207 Jun 18 22:19 dom4j-1.6.1-jboss.jar
-rwx------+ 1 jlaskowski None 2242529 Jun 19 11:18 hibernate-3.2.4.sp1.jar
-rwx------+ 1 jlaskowski None 60992 Jun 19 11:18 hibernate-validator-3.0.0.GA.jar
-rwx------+ 1 jlaskowski None 459663 Jun 18 22:19 javassist-3.3.GA.jar
-rwx------+ 1 jlaskowski None 133493 Jun 18 22:19 jboss-el-2.0.1.GA.jar
-rwx------+ 1 jlaskowski None 869271 Jun 18 22:19 jboss-seam-2.0.3.CR1.jar
Po zmianach w pomie od razu lepiej - komunikat poszedł sobie (zapewne zaraz, kiedy pozbyłem się el-api-1.0.jar).
 12:26:15,593 INFO  [Contexts] starting up: org.jboss.seam.security.facesSecurityEvents
12:26:15,593 INFO [Initialization] done initializing Seam
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.PRETTY_HTML' found, using default value true
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.ALLOW_JAVASCRIPT' found, using default value true
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.READONLY_AS_DISABLED_FOR_SELECTS' found, using default value true
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.RENDER_VIEWSTATE_ID' found, using default value true
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.STRICT_XHTML_LINKS' found, using default value true
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.CONFIG_REFRESH_PERIOD' found, using default value 2
12:26:15,625 INFO [MyfacesConfig] No context init parameter 'org.apache.myfaces.VIEWSTATE_JAVASCRIPT' found, using default value false
12:26:15,625 INFO [MyfacesConfig] Tomahawk jar not available. Autoscrolling, DetectJavascript, AddResourceClass and CheckExtensionsFilter are disabled now.
12:26:15,625 INFO [MyfacesConfig] Starting up Tomahawk on the MyFaces-JSF-Implementation
12:26:15,875 INFO [AbstractFacesInitializer] ServletContext
'C:\geronimo\repository\pl\jaceklaskowski\seam\seam-richfaces-tree\1.0\seam-richfaces-tree-1.0.war\' initialized.
Nie poprawiło to jednak wyświetlania strony - wciąż ten błąd z pustą stroną, która teoretycznie jest xhtmlową.

Spróbuję z jboss-seam-ui w pom.xml. Nic! Ponownie ten sam błąd! Temat zostawiam na później.

Kompletny pom.xml prezentuje się następująco:
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.jaceklaskowski.seam</groupId>
<artifactId>seam-richfaces-tree</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>seam-richfaces-tree Maven Webapp</name>
<url>http://www.jaceklaskowski.pl</url>
<repositories>
<repository>
<id>repository.jboss.org</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.org/maven2</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.seam</groupId>
<artifactId>jboss-seam</artifactId>
<version>2.0.3.CR1</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
</exclusion>
<exclusion>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
</exclusion>
<exclusion>
<groupId>javax.el</groupId>
<artifactId>el-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.seam</groupId>
<artifactId>jboss-seam-ui</artifactId>
<version>2.0.3.CR1</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>3.0.0.GA</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</exclusion>
<exclusion>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
<exclusion>
<groupId>asm</groupId>
<artifactId>asm-attrs</artifactId>
</exclusion>
<exclusion>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
</exclusion>
<exclusion>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>seam-richfaces-tree</finalName>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.10</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.geronimo.buildsupport</groupId>
<artifactId>geronimo-maven-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<assemblies>
<assembly>
<id>geronimo-jetty</id>
<groupId>org.apache.geronimo.assemblies</groupId>
<artifactId>geronimo-jetty6-javaee5</artifactId>
<version>2.1.1</version>
<classifier>bin</classifier>
<type>zip</type>
</assembly>
<assembly>
<id>geronimo-tomcat</id>
<groupId>org.apache.geronimo.assemblies</groupId>
<artifactId>geronimo-tomcat6-javaee5</artifactId>
<version>2.1.1</version>
<classifier>bin</classifier>
<type>zip</type>
</assembly>
</assemblies>
<defaultAssemblyId>geronimo-jetty</defaultAssemblyId>
<optionSets>
<optionSet>
<id>default</id>
<options>
<option>-XX:MaxPermSize=128m</option>
</options>
</optionSet>
<optionSet>
<id>morememory</id>
<options>
<option>-Xmx512m</option>
<option>-XX:MaxPermSize=128m</option>
</options>
</optionSet>
</optionSets>
</configuration>
<executions>
<execution>
<id>start-server</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start-server</goal>
</goals>
<configuration>
<assemblyId>geronimo-jetty</assemblyId>
<logOutput>true</logOutput>
<background>true</background>
<verifyTimeout>300</verifyTimeout>
<refresh>true</refresh>
<veryverbose>true</veryverbose>
<options>morememory</options>
</configuration>
</execution>
<execution>
<id>deploy-ear</id>
<phase>pre-integration-test</phase>
<goals>
<goal>deploy-module</goal>
</goals>
<configuration>
<moduleArchive>target/seam-richfaces-tree.war</moduleArchive>
<modulePlan>src/main/resources/geronimo-web.xml</modulePlan>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Dla dociekliwych kompletna aplikacja seam-richfaces-tree jako projekt mavenowy do pobrania jako seam-richfaces-tree.zip. Chętni do poprawek mile widziani.

Pytanie konkursowe: Jak nazywa się wtyczka mavenowa do zarządzania Apache Geronimo?

5 komentarzy:

  1. Gruszki w popiele sie zasypia a nie zasypuje...

    OdpowiedzUsuń
  2. Muszę przyznać, że komentarz należy do najlepszych, jakie ostatnio otrzymałem. Już miałem odpisać, że kompletnie się z tym nie zgadzam, bo brzmi jakoś pokracznie, a tu proszę, trochę szperania w Sieci i jest Wikisłownik o "nie zasypiać gruszek w popiele". Nawet nie przypuszczałem skąd pochodzi powiedzenie, a właśnie dzięki komentarzowi już wiem. Moje ostatnie dokonania technologiczne bledną w blasku takich komentarzy. Dziękuję za zwrócenie na to uwagi. Z nieukrywanym zniecierpliwieniem czekam na kolejne komentarze.

    OdpowiedzUsuń