578 lines
20 KiB
TeX
578 lines
20 KiB
TeX
\part{Allgemeines \& Config}
|
||
\section{Logging}
|
||
\subsubsection{Vorteile Logging mittels Framework (z.B.: log4j)}
|
||
\begin{itemize}
|
||
\item Nutzt ein einheitliches Format / Konventionen
|
||
\item logging kann optional an und ausgeschalten werden
|
||
\item durch verschiedene Log-level können Logs gefiltert erstellt werden
|
||
\item Layout für Ausgabe kann zentral definiert/geändert werden
|
||
\end{itemize}
|
||
\section{Annotationen}
|
||
\begin{itemize}
|
||
\item @MappedSuperclass
|
||
\begin{itemize}
|
||
\item ist im Hybernate Framework
|
||
\item eine Klasse durch die gemeinsame Felder definiert werden.
|
||
\item definiert eine abstrakte Superklasse
|
||
\end{itemize}
|
||
\item @Produces
|
||
\begin{itemize}
|
||
\item kommt während deployment, markiert Factory Method damit man nicht direkt auf die Klasse zugreifen muss
|
||
\end{itemize}
|
||
\item @Typed
|
||
\begin{itemize}
|
||
\item zeigt die Vererbung Wieso bei uns allein stehend?
|
||
\end{itemize}
|
||
\item @Named
|
||
\begin{itemize}
|
||
\item Zeigt bei Mehrdeutigkeit das richtige Objekt mit dem Namen
|
||
\end{itemize}
|
||
\item @Resource
|
||
\begin{itemize}
|
||
\item fast wie Dependency Injection
|
||
\item damit weiß der Container wie er das Annotierte Feld instanzieren muss
|
||
\end{itemize}
|
||
\item @Stateless
|
||
\begin{itemize}
|
||
\item speichert den Client Status nicht
|
||
\end{itemize}
|
||
\item @Entity
|
||
\begin{itemize}
|
||
\item Data Access Layer
|
||
\end{itemize}
|
||
\item @Table
|
||
\begin{itemize}
|
||
\item Tabellenname im SQL
|
||
\end{itemize}
|
||
\item @Column
|
||
\begin{itemize}
|
||
\item SQL-Spalten nullable=false
|
||
\end{itemize}
|
||
\item @OneToMany
|
||
\item @JoinColums
|
||
\begin{itemize}
|
||
\item welche Spalten zusammen gehören FK
|
||
\end{itemize}
|
||
\item @OneToMany
|
||
\begin{itemize}
|
||
\item auf anderen Seite
|
||
\end{itemize}
|
||
\item @ApplicationScoped
|
||
\begin{itemize}
|
||
\item lebt die ganze Applikation lang, wird einmal gemacht.
|
||
\end{itemize}
|
||
\item @PersistenceContext
|
||
\begin{itemize}
|
||
\item persistance.xml auslesen für Treiber und andere JPA Geschichten + Data Source. Entity Manager
|
||
\end{itemize}
|
||
\item @Id
|
||
\begin{itemize}
|
||
\item das ist die id
|
||
\end{itemize}
|
||
\item @GeneratedValue
|
||
\begin{itemize}
|
||
\item Wert kommt aus der DB
|
||
\end{itemize}
|
||
\item @Local
|
||
\begin{itemize}
|
||
\item Klasse für lokale Aufrufe.
|
||
\end{itemize}
|
||
\item @Remote
|
||
\begin{itemize}
|
||
\item interprozessaufrufe. RMI
|
||
\end{itemize}
|
||
\item @ApplicationException
|
||
\begin{itemize}
|
||
\item Rollback wenn so eine Exception kommt, Nachricht zum Client.
|
||
\end{itemize}
|
||
\end{itemize}
|
||
|
||
|
||
\subsection{Annotationen - Details}
|
||
\begin{itemize}
|
||
\item CascadeType.Remove löscht die damit Annotierte Verknüpfung mit
|
||
\item dies geht auch rekursiv in der kompletten Datenbank
|
||
\item CascadeType.Remove und orphanRemoval=true ist equivalent
|
||
\end{itemize}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
...
|
||
@OneToOne(fetch = FetchType.EAGER,
|
||
cascade = CascadeType.ALL,
|
||
orphanRemoval = true)
|
||
@JoinColumn(name = "documentlibrary_id")
|
||
private Documentlibrary documentlibrary;
|
||
|
||
@Column(nullable = false, unique = true)
|
||
private String name;
|
||
...
|
||
public enum CascadeType {
|
||
/* Cascade all operations /
|
||
ALL,
|
||
/* Cascade persist operation /
|
||
PERSIST,
|
||
/* Cascade merge operation /
|
||
MERGE,
|
||
/* Cascade remove operation /
|
||
REMOVE,
|
||
/* Cascade refresh operation /
|
||
REFRESH,
|
||
*/
|
||
DETACH
|
||
...
|
||
}
|
||
)
|
||
\end{minted}
|
||
|
||
\section{Konfigurationsdateien}
|
||
\subsection{persistence.xml}
|
||
|
||
Die Datei \emph{persistence.xml} ist der zentrale Bestandteil der Persistierungs-Konfiguration.
|
||
|
||
Folgende Dinge können konfiguriert werden:
|
||
|
||
\begin{itemize}
|
||
\item SQL dialect
|
||
\item the persistence provider that shall be used at runtime
|
||
\item the data source you want to use to connect to your database
|
||
\item several provider-specific configuration parameters
|
||
\end{itemize}
|
||
|
||
|
||
\begin{code}
|
||
\captionof{listing}{persistence.xml}
|
||
|
||
|
||
\begin{minted}[linenos,breaklines=true]{xml}
|
||
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<persistence version="2.0"
|
||
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_2_0.xsd">
|
||
|
||
<persistence-unit name="primary">
|
||
<jta-data-source>java:jboss/datasources/psoeDS</jta-data-source>
|
||
<properties>
|
||
<!-- Properties for Hibernate -->
|
||
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
|
||
<property name="hibernate.enable_lazy_load_no_trans" value="true" />
|
||
<!--
|
||
SQL stdout logging
|
||
-->
|
||
<property name="hibernate.show_sql" value="true"/>
|
||
<property name="hibernate.format_sql" value="false"/>
|
||
<property name="use_sql_comments" value="true"/>
|
||
|
||
<!--
|
||
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
|
||
-->
|
||
</properties>
|
||
</persistence-unit>
|
||
|
||
</persistence>
|
||
|
||
\end{minted}
|
||
\end{code}
|
||
|
||
|
||
\begin{figure}[!htp]
|
||
\centering
|
||
\includegraphics[width=0.7\textwidth]{pics/ConfigFiles.png}
|
||
\end{figure}
|
||
\subsection{web.xml}
|
||
\begin{itemize}
|
||
\item konfiguriert den Java Webserver (Wildfly - JBOSS)
|
||
\item Einbindung des Faces-Servlet (FrontController - Implementierung, Zugriffskontrolle, Rollenkonfiguration)
|
||
\item befindet sich im Ordner \textbf{src/main/webapp/WEB-INF/web.xml}
|
||
\end{itemize}
|
||
\begin{minted}[linenos,breaklines=true]{xml}
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
|
||
|
||
<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>*.xhtml</url-pattern>
|
||
</servlet-mapping>
|
||
|
||
<!-- Security roles -->
|
||
<security-role>
|
||
<description>administrators</description>
|
||
<role-name>ADMIN</role-name>
|
||
</security-role>
|
||
|
||
<!-- Security constraints -->
|
||
<security-constraint>
|
||
<web-resource-collection>
|
||
<web-resource-name>admin area</web-resource-name>
|
||
<url-pattern>/admin/*</url-pattern>
|
||
</web-resource-collection>
|
||
<auth-constraint>
|
||
<role-name>ADMIN</role-name>
|
||
</auth-constraint>
|
||
</security-constraint>
|
||
|
||
<login-config>
|
||
<auth-method>FORM</auth-method>
|
||
<realm-name>pse</realm-name>
|
||
<form-login-config>
|
||
<form-login-page>/login.xhtml</form-login-page>
|
||
<form-error-page>/login.xhtml</form-error-page>
|
||
<!-- <form-error-page>/loginerror.xhtml</form-error-page> -->
|
||
</form-login-config>
|
||
</login-config>
|
||
</web-app>
|
||
\end{minted}
|
||
\subsection{pom.xml}
|
||
\begin{itemize}
|
||
\item \textit{resources}-plugin (bindet die Serverressourcen ein - Ordner \textit{configuration} im Projekt - z.B. \textit{standalone-psoe.xml})
|
||
\item Wildfly (JBoss)– Webserver
|
||
\begin{multicols}{2}
|
||
\begin{enumerate}
|
||
\item Compile
|
||
\item Surefire (unitTests)
|
||
\item Packaging - war file erstellen
|
||
\item Wildfly - fressen und deployen
|
||
\item Failsafe IT-test
|
||
\item MVN site
|
||
\item Gui test
|
||
\end{enumerate}
|
||
\end{multicols}
|
||
\item Primeface = jsf Framework
|
||
\item Jacoco = test Coverage
|
||
\item Slf4j = logger
|
||
\item Jaxb – xml
|
||
\item Cdi = context dependancy injection
|
||
\end{itemize}
|
||
\subsubsection{Aufbau pom.xml}
|
||
\begin{figure}[h]
|
||
\centering
|
||
\includegraphics[width=0.3\linewidth]{pics/pom-structure}
|
||
\includegraphics[width=0.3\linewidth]{pics/pom-properties}
|
||
\label{fig:pom}
|
||
\end{figure}
|
||
|
||
\subsection{standalone-psoe.xml}
|
||
|
||
Wird ein JBoss Applikationsserver im \emph{standalone}-Modus betrieben, läuft jede Instanz in einem eigenen Prozess.
|
||
Diese Datei ist eine Java Enterprise Edition 6 zertifizierte Web-Profil Konfiguration welche alle benötigten Technologien (z.B. Extensions von JBoss, Datasources etc.) definiert.
|
||
|
||
JBoss EAP benutzt standardmäßig die standalone.xml Konfigurationsdatei, kann aber auch unter Verwendung einer anderen gestartet werden.
|
||
|
||
Abschnitte der standalone.xml
|
||
\begin{itemize}
|
||
\item extensions (z.B. diverse Wildfly Module)
|
||
\item management (z.B. Access Control -> role-mapping)
|
||
\item profile (z.B. JPA Subsystem)
|
||
\item interfaces (z.B. \${jboss.bind.address:127.0.0.1})
|
||
\item socket-binding-group (z.B \${jboss.http.port:8080})
|
||
\item Rechte (Management-Realm)
|
||
\item Datenbankzugriffsparameter
|
||
\end{itemize}
|
||
|
||
\begin{code}
|
||
\captionof{listing}{standalone.xml (auszugsweise)}
|
||
|
||
|
||
\begin{minted}[linenos,breaklines=true]{xml}
|
||
|
||
........
|
||
|
||
</endpoint-config>
|
||
<client-config name="Standard-Client-Config"/>
|
||
</subsystem>
|
||
<subsystem xmlns="urn:jboss:domain:weld:3.0"/>
|
||
</profile>
|
||
<interfaces>
|
||
<interface name="management">
|
||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||
</interface>
|
||
<interface name="public">
|
||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||
</interface>
|
||
</interfaces>
|
||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
|
||
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
|
||
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
|
||
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
|
||
<socket-binding name="http" port="${jboss.http.port:8080}"/>
|
||
<socket-binding name="https" port="${jboss.https.port:8443}"/>
|
||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||
<socket-binding name="txn-status-manager" port="4713"/>
|
||
<outbound-socket-binding name="mail-smtp">
|
||
<remote-destination host="localhost" port="25"/>
|
||
</outbound-socket-binding>
|
||
</socket-binding-group>
|
||
</server>
|
||
|
||
\end{minted}
|
||
\end{code}
|
||
|
||
\subsection{log4j.properties}
|
||
textit{src/test/resources/log4j.properties}
|
||
|
||
\section{Fehler im Projekt}
|
||
\subsection{Return null}
|
||
Anstatt von Null einfach eine Leere Liste bzw. ein default Objekt (oder new <Object>) zurückgeben
|
||
\subsection{Exception nicht gefangen}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
package at.fhj.swd.psoe.service.impl;
|
||
...
|
||
@Override
|
||
public void removeCommunityByAdmin(CommunityDTO communityDTO) {
|
||
Community community = communityDAO.findById(communityDTO.getId());
|
||
String errorText;
|
||
try {
|
||
communityDAO.removeCommunityByAdmin(community);
|
||
} catch (DaoException e) {
|
||
errorText = "Error removing community";
|
||
logger.error(errorText, e);
|
||
throw new ServiceException(errorText);
|
||
}
|
||
}
|
||
\end{minted}
|
||
\subsection{Destructive Wrapping - Logging fehlt - Information geht verloren}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
package at.fhj.swd.psoe.service.impl;
|
||
...
|
||
@Override
|
||
public CommunityDTO updateCommunityEnabled(String adminUserId, String communityName, boolean isEnabled) {
|
||
String errorText = "";
|
||
try {
|
||
boolean hasPermission = false;
|
||
User adminUser = userDao.getByUserId(adminUserId);
|
||
Community community = communityDAO.getByName(communityName);
|
||
|
||
|
||
for (Role r : adminUser.getRoles()) {
|
||
if (r.getname().equals("ADMIN") || r.getname().equals("PORTALADMIN")) {
|
||
hasPermission = true;
|
||
}
|
||
}
|
||
if (hasPermission || adminUser.getUserId().equals(community.getCommunityAdminUser().getUserId())) {
|
||
community.setIsEnabled(isEnabled);
|
||
communityDAO.update(community);
|
||
} else {
|
||
errorText = "No Permission to update community";
|
||
throw new AuthenticationException(errorText);
|
||
}
|
||
return CommunityMapper.toDTO(community);
|
||
} catch (DaoException e) {
|
||
errorText = "Error updating community enabled";
|
||
throw new ServiceException(errorText);
|
||
} catch (AuthenticationException e) {
|
||
throw new ServiceException(errorText);
|
||
} catch (Throwable e) {
|
||
errorText = "Unknown error updating community enabled";
|
||
throw new ServiceException(errorText);
|
||
}
|
||
}
|
||
\end{minted}
|
||
\subsection{Logger mit falschem Parameter}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
package at.fhj.swd.psoe.service.impl;
|
||
...
|
||
@Stateless
|
||
public class DepartmentHierarchyServiceImpl implements DepartmentHierarchyService, Serializable {
|
||
|
||
private static final long serialVersionUID = -2467949382018996094L;
|
||
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
|
||
...
|
||
\end{minted}
|
||
\subsection{Fehlendes Exception Handling}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
package at.fhj.swd.psoe.service.impl;
|
||
...
|
||
@Local(MessageService.class)
|
||
@Remote(MessageServiceRemote.class)
|
||
@Stateless
|
||
public class MessageServiceImpl implements MessageService, MessageServiceRemote, Serializable {
|
||
...
|
||
@Override
|
||
public MessageDTO getByMessageId(long id) {
|
||
Message message = messageDAO.getById(id);
|
||
if (message == null) {
|
||
return null;
|
||
}
|
||
return MessageMapper.toDTO(message);
|
||
}
|
||
|
||
@Override
|
||
public MessageDTO updateByAdmin(long id, String content, Date changed) {
|
||
Message message = messageDAO.getById(id);
|
||
message.setContent(content);
|
||
message.setEditedByAdmin(changed);
|
||
return MessageMapper.toDTO(messageDAO.update(message));
|
||
}
|
||
|
||
@Override
|
||
public MessageDTO updateByUser(long messageId, String content, Date changed) {
|
||
Message message = messageDAO.getById(messageId);
|
||
message.setContent(content);
|
||
message.setEditedByUser(changed);
|
||
return MessageMapper.toDTO(messageDAO.update(message));
|
||
}
|
||
\end{minted}
|
||
\subsection{Exception werfen und gleich wieder fangen}
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
...
|
||
public class MessageServiceImpl implements MessageService, MessageServiceRemote, Serializable {
|
||
private static final long serialVersionUID = 6768291437557855130L;
|
||
...
|
||
// nicht optimal, da die IllegalArgumentException gleich wieder gefangen wird
|
||
// überdies wird alles andere nicht gefangen
|
||
|
||
|
||
@Override
|
||
public void deleteMessage(long id) {
|
||
try {
|
||
Message message = messageDAO.getById(id);
|
||
if (message == null) {
|
||
throw new IllegalArgumentException("Message cannot be empty");
|
||
}
|
||
messageDAO.delete(message);
|
||
logger.info("Message deleted successfully");
|
||
}
|
||
catch (IllegalArgumentException ex) {
|
||
String errorMsg = "Could not delete the message (illegal argument)";
|
||
logger.error(errorMsg, ex);
|
||
throw new ServiceException(errorMsg);
|
||
}
|
||
}
|
||
|
||
//---------- besser wäre
|
||
package at.fhj.swd.psoe.service.impl;
|
||
...
|
||
// erst loggen, dass man in die Methode gekommen ist
|
||
// wenn userDTO null ist wird IllegalArgument geworfen und das außerhalb des try catch blocks
|
||
// erst wird die DaoException gefangen und anschließend alle anderen
|
||
// Stacktrace wird geloggt und jeweils die ServiceException weitergegeben
|
||
@Override
|
||
public void saveUser(UserDTO userDTO) {
|
||
logger.debug("UserService saveUser() called with parameter: '{}'", userDTO);
|
||
|
||
if (userDTO == null) {
|
||
throw new IllegalArgumentException("userDTO is null");
|
||
}
|
||
|
||
try {
|
||
User user = (userDTO.getId() == null) ? new User() : userDao.getById(userDTO.getId());
|
||
userDao.update(UserMapper.toEntity(userDTO, user));
|
||
} catch (DaoException e) {
|
||
logger.error("Error saving user", e);
|
||
throw new ServiceException("Error saving user");
|
||
} catch (Throwable e) {
|
||
logger.error("Unknown error saving user", e);
|
||
throw new ServiceException("Unknown error saving user");
|
||
}
|
||
}
|
||
\end{minted}
|
||
\section{Tests}
|
||
\subsection{Testpyramide}
|
||
\begin{figure}[h]
|
||
\centering
|
||
\includegraphics[width=0.9\linewidth]{pics/testpyramide}
|
||
\caption{}
|
||
\label{fig:testpyramide}
|
||
\end{figure}
|
||
\subsection{Unit}
|
||
\subsection{Integration}
|
||
\subsection{Selenium bzw. GUI}
|
||
|
||
\section{Toni FRAAGNAA}
|
||
Den Code durchgehen - was statt null - welche Exception - logger ok?
|
||
ob ein Throw im try Block ok ist.
|
||
\begin{minted}[linenos,breaklines=true]{java}
|
||
|
||
@Override
|
||
public List<Document> findByUser(User user) {
|
||
logger.debug("dao: documents findByUser");
|
||
try {
|
||
if(user == null) throw new IllegalArgumentException("user must not be null");
|
||
return entityMangaer.createQuery("from Document d where d.user.userId = :userid", Document.class).setParameter("userid", user.getUserId()).getResultList();
|
||
} catch(NoResultException noresex) {
|
||
logger.info("dao: findByUser no documents found");
|
||
return null;
|
||
}
|
||
catch (Exception e) {
|
||
throw new DaoException("error finding documents of user",e);
|
||
}
|
||
}
|
||
|
||
// throw in try
|
||
@Override
|
||
public void delete(Document document) {
|
||
logger.debug("dao: delete document");
|
||
|
||
try {
|
||
if(document == null) throw new IllegalArgumentException("document must not be null");
|
||
if(!entityMangaer.contains(document)) {
|
||
document = entityMangaer.merge(document);
|
||
}
|
||
entityMangaer.remove(document);
|
||
}
|
||
catch (Exception e) {
|
||
throw new DaoException("error deleting document",e);
|
||
}
|
||
}
|
||
// ist loggen ohne stacktrace ok?
|
||
@Override
|
||
public TimeRecording getTimeRecordingByTask(Task task) {
|
||
TimeRecording result = new TimeRecording();
|
||
try {
|
||
result = entityManager.createQuery("select t from TimeRecording t where t.task.id = :id", TimeRecording.class)
|
||
.setParameter("id", task.getId()).getSingleResult();
|
||
} catch (NoResultException e) {
|
||
logger.warn("Not Possible to find TimeRecording with task id " + task.getId());
|
||
}
|
||
return result;
|
||
}
|
||
// UserDTO - @XmlElement
|
||
@XmlElement
|
||
public void setPhoneNumber(String phoneNumber) {
|
||
this.phoneNumber = phoneNumber;
|
||
}
|
||
// wos is produces
|
||
@Override
|
||
@Produces
|
||
@Named("userPrincipal")
|
||
@SessionScoped
|
||
public UserPrincipal getUserPrincipal() {
|
||
String principalName = sessionContext.getCallerPrincipal().getName();
|
||
logger.info(principalName);
|
||
try {
|
||
User user = userDao.getByEmail(principalName);
|
||
return new UserPrincipal(user.getId(), user.getUserId(), user.getEmail(), user.getFirstname(), user.getLastname());
|
||
} catch (DaoException e) {
|
||
logger.error("Error loading user '{}'", principalName, e);
|
||
throw new ServiceException("Error loading user");
|
||
} catch (Throwable e) {
|
||
logger.error("Unknown error loading user '{}'", principalName, e);
|
||
throw new ServiceException("Unknown error loading user");
|
||
}
|
||
// @Typed -> Zarwos
|
||
|
||
// warum immer mappedBy Mehrzahl
|
||
@ManyToMany(mappedBy = "businessTrips")
|
||
|
||
// dependency Injection
|
||
|
||
// braucht man im Controller (ViewHelper) überhaupt noch Exception Handling
|
||
|
||
\end{minted}
|
||
|
||
\section{Frageart Prüfung}
|
||
Welche Fehler können bei Exception-Handling vorkommen in unserem Projekt?? – wie funktioniert es grundsätzlich in unserem Code
|
||
|
||
DocumentDAO – DocumentService – DocumentController – so sollte Exception-Handling implementiert warden
|
||
|
||
DAO wirft Exception – im ServiceLayer wird dies gefangen und der Stack-Trace wird im weggeloggt und eine benutzerfreundliche Fehlermeldung wird ausgegeben (Destructive Wrapping).
|
||
|
||
Alle Patterns, die vorkommen – praktische Beispiele aus dem Code
|
||
|
||
Was sind JavaBeans? Wie funktioniert das Konzept? Wie wird es genau implementiert?
|
||
NamedBean, TypedBean etc.
|