Welcome to the Jspresso Wiki !
Short answer: no.
Long one: use two different browser instances (e.g. one IE and one Firefox) or erase the cookies of your current browser.
Short answer: no.
Long one: as long as you keep your model unchanged, there is no need to launch a maven package task.
Very long one: even some change in the model doesn't need it. It's the case of any change that doesn't imply changes of the entity or component java classes or the linked Hibernate files (see core/target/generated-sources/entitygenerator and core/target/generated-resourcesxdoclet). E.g.: you can change the value of an entry in serviceDelegateClassNames or serviceDelegateBeanNames, but not the key. Same thing with the values in a renderedProperties.
Create a new project with the maven command:
mvn archetype:generate -DarchetypeCatalog=http://repository.jspresso.org/maven2-snapshots/
You'll find on this page links towards forum threads about issues which are not thoroughly treated either in the documentation or in the wiki yet.
It's a live page:
- as soon as an issue is integrated in the documentation, the wiki or the FAQ, it should be removed from here;
- new theads can be added if they give either a good explanation or a clear how to;
- some links just go to the main entry of a thread: they could be improved by going directly to the best part of the thread;
- if you think that some good stuff is missing...
And now, the winners are:
Miscellaneous
http://www.jspresso.org/forum/workspace-modules-and-view
http://www.jspresso.org/forum/properties-top-tabs-feature-request
http://www.jspresso.org/forum/request-no-label
http://www.jspresso.org/forum/more-explanation-needed-initializationmapping
http://www.jspresso.org/forum/selected-items#comment-50
Lifecycle
http://www.jspresso.org/forum/selection-and-wizardaction
http://www.jspresso.org/forum/change-property-collections-item#comment-78
DB legacy
http://www.jspresso.org/forum/using-jspresso-existing-db-schema#comment-790
http://www.jspresso.org/forum/using-jspresso-existing-db-schema#comment-780
http://www.jspresso.org/forum/entity-annotations#comment-498
http://www.jspresso.org/forum/sql-name-table-columns
http://www.jspresso.org/forum/existing-domain-model
http://www.jspresso.org/forum/entity-annotations
Mail
http://www.jspresso.org/forum/action-sending-mail
Data exportation
http://www.jspresso.org/forum/help-how-export-data
Reporting
http://www.jspresso.org/forum/how-integrate-itext-jspresso
Extension
http://www.jspresso.org/forum/widget-set
http://www.jspresso.org/forum/how-integrate-itext-jspresso
Login
http://www.jspresso.org/forum/jaas-jdbc-login
http://www.jspresso.org/forum/help-multiple-users-development
http://www.jspresso.org/forum/solved-logincontext-problem
http://www.jspresso.org/forum/help-ldap-and-custom-properties
Wizard
http://www.jspresso.org/forum/help-how-initialize-value-wizard-model
http://www.jspresso.org/forum/front-action-get-value
http://www.jspresso.org/forum/question-staticwizardstepdescriptor
http://www.jspresso.org/forum/help-choice-wizard
Card view
http://www.jspresso.org/forum/dynamic-selection-out-multiple-views-featu...
http://www.jspresso.org/forum/dynamic-panel
Gates
http://www.jspresso.org/forum/dynamic-selection-out-multiple-views-featu...
http://www.jspresso.org/forum/request-writabilitygate-based-enumerated-type
http://www.jspresso.org/forum/bug-readabilitygate-does-not-seem-work
with the question of its usability...
http://www.jspresso.org/forum/problem-gates-advanced
http://www.jspresso.org/forum/help-actions-entity-selections-and-entity-...
http://www.jspresso.org/forum/help-property-descriptor-and-readibility-g...
http://www.jspresso.org/forum/bug-strange-model-collection-gate
http://www.jspresso.org/forum/help-writabilitygate-table
http://www.jspresso.org/forum/bug-readabilitygate-does-not-seem-work
Show/Edit detail view
http://www.jspresso.org/forum/avidavid-edit-button-problem
http://www.jspresso.org/forum/maxime-edit-button-problem
http://www.jspresso.org/forum/edit-button-master-detail-action-map-and-d...
http://www.jspresso.org/forum/add-button-open-entity
http://www.jspresso.org/forum/adding-masterdetail-actionmap
http://www.jspresso.org/forum/double-click-collection-view-based-relatio...
The idea is to add some new model types in a jspresso project with a minimal effort. Also:
- the types are simple ones, i.e. with only one value to deal with,
- and there would be no need to work at the UI level.
Luckily I only need to use the joda types LocalDate and LocalDateTime in a jspresso project. This use case shows how to implement such simple types. With more complex type like Joda Interval, it'd have been an other story!
To do such a thing we need to deal with five issues:
Now the new types can be used in place of standard date, with the same syntax and the same attributes. To do it, just specify the new interfaces in the model configuration file, e.g.:
<bean
id="dateType"
class="org.popsuite.framework.model.descriptor.basic.BasicLocalDatePropertyDescriptor"
abstract="true" />
[...]
<bean parent="dateType">
<property name="name"
value="paymentDay" />
</bean>
To be used without limitation!
A model descriptor is agnostic in regard of the UI technology. Each new model type needs only one descriptor. At the moment, only Joda Date and DateTime are implemented.
For each type we need an interface and at least one implementation. There is no need to deal with UI (until we want to enhance the current presentation of a date...). So the new interfaces and implementations are simply derived from the current date ones.
First the two interfaces:
package org.popsuite.framework.model.descriptor;
import org.jspresso.framework.model.descriptor.IDatePropertyDescriptor;
public interface ILocalDateTimePropertyDescriptor extends IDatePropertyDescriptor {}
and
package org.popsuite.framework.model.descriptor;
public interface ILocalDatePropertyDescriptor extends ILocalDateTimePropertyDescriptor {}
Then an implementation:
package org.popsuite.framework.model.descriptor.basic;
import org.joda.time.LocalDateTime;
import org.jspresso.framework.model.descriptor.EDateType;
import org.jspresso.framework.model.descriptor.basic.BasicDatePropertyDescriptor;
import org.popsuite.framework.model.descriptor.ILocalDateTimePropertyDescriptor;
public class BasicLocalDateTimePropertyDescriptor extends BasicDatePropertyDescriptor
implements ILocalDateTimePropertyDescriptor {
public BasicLocalDateTimePropertyDescriptor() {
super();
}
@Override
public Class<?> getModelType() {
return LocalDateTime.class;
}
@Override
public EDateType getType() {
if (super.getType() != null) {
return super.getType();
}
return null;
}
}
and
package org.popsuite.framework.model.descriptor.basic;
import org.joda.time.LocalDate;
import org.jspresso.framework.model.descriptor.EDateType;
import org.popsuite.framework.model.descriptor.ILocalDatePropertyDescriptor;
public class BasicLocalDatePropertyDescriptor extends BasicLocalDateTimePropertyDescriptor
implements ILocalDatePropertyDescriptor {
public BasicLocalDatePropertyDescriptor() {}
@Override
public Class<?> getModelType() {
return LocalDate.class;
}
@Override
public EDateType getType() {
return EDateType.DATE;
}
}
Only the Swing and Wings versions of jspresso need the creation of new dedicated connectors. For the others remote UI, Flex and Qooxdoo, a connector factory is used.
package org.popsuite.framework.binding.swing;
import static org.popsuite.framework.model.specialcase.SpecialDates.isUnknown;
import java.text.ParseException;
import java.util.Date;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.jspresso.framework.binding.ConnectorBindingException;
import org.jspresso.framework.binding.swing.JDateFieldConnector;
import org.jspresso.framework.gui.swing.components.JDateField;
/**
* JJodaTimeField connector.
*
* Manages LocalDate and LocalDateTime values.
*
*/
public class JJodaTimeFieldConnector extends JDateFieldConnector {
/**
* Constructs a new <code>JDateField</code> instance.
*
* @param id
* the connector identifier.
* @param dateField
* the connected JDateField.
*/
public JJodaTimeFieldConnector(String id, JDateField dateField) {
super(id, dateField);
}
/**
* Will be called by <code>setConnecteeValue</code>.
*
* {@inheritDoc}
*/
@Override
protected void protectedSetConnecteeValue(Object aValue) {
Object result = aValue;
if (aValue instanceof LocalDateTime) {
result = isUnknown(aValue) ? null : ((LocalDateTime) aValue).toDateTime().toDate();
} else if (aValue instanceof LocalDate) {
result = isUnknown(aValue) ? null : ((LocalDate) aValue).toDateTimeAtStartOfDay().toDate();
}
super.protectedSetConnecteeValue(result);
}
/**
* Gets the value out of the connector text after parsing the string
* representation.
* <p>
* {@inheritDoc}
*/
@Override
protected Object getConnecteeValue() {
// don't call getValue() due to bad focus event delivery order of
// JFormattedTextField.
// return getConnectedJComponent().getValue();
try {
if (getConnectedJComponent().getFormattedTextField().getText() = = null
|| getConnectedJComponent().getFormattedTextField().getText().length() = = 0) {
return null;
}
Object result = getConnectedJComponent().getFormattedTextField().getFormatter()
.stringToValue(getConnectedJComponent().getFormattedTextField().getText());
Class<?> expectedType = null;
if (getModelConnector().getModelDescriptor() != null) {
expectedType = getModelConnector().getModelDescriptor().getModelType();
} else {
expectedType = getModelDescriptor().getModelType();
}
return (LocalDateTime.class.equals(expectedType))
? LocalDateTime.fromDateFields((Date) result)
: LocalDate.fromDateFields((Date) result);
} catch (ParseException ex) {
throw new ConnectorBindingException(ex);
}
}
}
package org.popsuite.framework.binding.wings;
import static org.popsuite.framework.model.specialcase.SpecialDates.isUnknown;
import java.util.Date;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.jspresso.framework.binding.wings.XCalendarConnector;
import org.wingx.XCalendar;
public class XJodaTimeConnector extends XCalendarConnector{
public XJodaTimeConnector(String id, XCalendar dateField) {
super(id, dateField);
}
/**
* Sets the value to the connector text after formatting the string
* representation.
* <p>
* {@inheritDoc}
*/
@Override
public void setConnecteeValue(Object aValue) {
Date result = null;
if (aValue instanceof LocalDate) {
result = isUnknown(aValue) ? null : ((LocalDate) aValue).toDateTimeAtStartOfDay().toDate();
}
else if(aValue instanceof LocalDateTime) {
result = isUnknown(aValue) ? null : ((LocalDateTime) aValue).toDateTime().toDate();
}
else {
result = (Date) aValue;
}
getConnectedSComponent().setDate(result);
}
/**
* Gets the value out of the connector text after parsing the string
* representation.
* <p>
* {@inheritDoc}
*/
@Override
protected Object getConnecteeValue() {
Object result = getConnectedSComponent().getDate();
if (null = = result) return null;
Class<?> expectedType = null;
if (getModelConnector().getModelDescriptor() != null) {
expectedType = getModelConnector().getModelDescriptor().getModelType();
} else {
expectedType = getModelDescriptor().getModelType();
}
result = (LocalDateTime.class.equals(expectedType))
? LocalDateTime.fromDateFields((Date) result)
: LocalDate.fromDateFields((Date) result);
return result;
}
}
A connector factory is used with the two other remote UI, Flex and Qooxdoo. This factory generates JodaValueConnectors.
The factory:
package org.popsuite.framework.binding.remote;
import org.jspresso.framework.binding.IValueConnector;
import org.jspresso.framework.binding.remote.RemoteConnectorFactory;
import org.jspresso.framework.binding.remote.RemoteValueConnector;
public class JodaRemoteConnectorFactory extends RemoteConnectorFactory{
@Override
public IValueConnector createValueConnector(String id) {
RemoteValueConnector connector = new JodaRemoteValueConnector(id, this);
attachListeners(connector);
return connector;
}
}
And the value connector:
package org.popsuite.framework.binding.remote;
import static org.popsuite.framework.model.specialcase.SpecialDates.isUnknown;
import java.util.Date;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.jspresso.framework.binding.remote.RemoteConnectorFactory;
import org.jspresso.framework.binding.remote.RemoteValueConnector;
public class JodaRemoteValueConnector extends RemoteValueConnector{
public JodaRemoteValueConnector(String id,
RemoteConnectorFactory connectorFactory) {
super(id, connectorFactory);
}
/**
* {@inheritDoc}
*/
@Override
protected Object getValueForState() {
Object result = super.getValueForState();
if (result instanceof LocalDate) {
result = isUnknown(result) ? null : ((LocalDate) result).toDateTimeAtStartOfDay().toDate();
}
else if(result instanceof LocalDateTime) {
result = isUnknown(result) ? null : ((LocalDateTime) result).toDateTime().toDate();
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
protected void setConnecteeValue(Object connecteeValue) {
Object result = connecteeValue;
if (result instanceof Date)
{
Class<?> expectedType = getModelConnector().getModelDescriptor().getModelType();
if (LocalDateTime.class.equals(expectedType)) {
result = LocalDateTime.fromDateFields((Date) result);
} else if (LocalDate.class.equals(expectedType)) {
result = LocalDate.fromDateFields((Date) result);
}
}
super.setConnecteeValue(result);
}
}
The framework popsuite defined unknown values for the dates. Such values must be intercepted by the connectors (see above).
Note: a use case of how to implement unknown values with jspresso will be the topic of a future wiki entry. 
package org.popsuite.framework.model.specialcase;
import java.util.Date;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
public class SpecialDates {
public static final LocalDate UNKNOWN_DATE =
new LocalDate().year().setCopy(new LocalDate().year().getMinimumValueOverall())
.dayOfYear().withMaximumValue();
public static final LocalDate UNKNOWN_BEGIN = UNKNOWN_DATE;
public static final LocalDate UNKNOWN_END =
new LocalDate().year().setCopy(new LocalDate().year().getMaximumValueOverall())
.dayOfYear().withMinimumValue();
public static boolean isUnknown(Object date)
{
if (null = = date) return true;
if (date instanceof Date) return false;
if(date instanceof LocalDate) {
return ! UNKNOWN_BEGIN.isBefore( (LocalDate) date) || ! UNKNOWN_END.isAfter( (LocalDate) date) ;
}
else if (date instanceof LocalDateTime) {
return ! (new LocalDateTime(UNKNOWN_BEGIN)).isBefore( (LocalDateTime) date)
|| ! (new LocalDateTime(UNKNOWN_END)).isAfter( (LocalDateTime) date) ;
}
return true;
}
}
Each UI needs a specific view factory, but Flex and Qooxdoo share the same one. These factories are derived from the standard ones. The former must be used in placed of the latter in the frontend configuration file.
package org.popsuite.framework.view.swing;
[...]
/**
* Enhanced view factory:
* - manages the joda's dates
*
*/
public class PopsuiteSwingViewFactory extends DefaultSwingViewFactory {
private static final Date TEMPLATE_DATE = new Date(27166271000L);
@Override
protected IView<JComponent> createPropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
IView<JComponent> view = null;
IPropertyDescriptor propertyDescriptor = (IPropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
if (propertyDescriptor instanceof ILocalDatePropertyDescriptor) { // must come before ILocalDateTimePropertyDescriptor
view = createLocalDatePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
else if (propertyDescriptor instanceof ILocalDateTimePropertyDescriptor) {
view = createLocalDateTimePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
if (view = = null) {
return super.createPropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
decorateWithDescription(propertyDescriptor, locale, view); // see AbstractViewFactory.createPropertyView
return view;
}
protected TableCellRenderer createTableCellRenderer(IPropertyDescriptor propertyDescriptor, Locale locale) {
if (propertyDescriptor instanceof ILocalDatePropertyDescriptor) { // must come before ILocalDateTimePropertyDescriptor
return createDateTableCellRenderer(
(ILocalDatePropertyDescriptor) propertyDescriptor,
locale);
}
else if (propertyDescriptor instanceof ILocalDateTimePropertyDescriptor) {
return createDateTableCellRenderer(
(ILocalDateTimePropertyDescriptor) propertyDescriptor,
locale);
}
return super.createTableCellRenderer(propertyDescriptor, locale);
}
protected IView<JComponent> createLocalDatePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
ILocalDatePropertyDescriptor propertyDescriptor = (ILocalDatePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
JDateField viewComponent = createJDateField(locale);
DateFormat format = createDateFormat(propertyDescriptor, locale);
viewComponent.getFormattedTextField().setFormatterFactory(
new DefaultFormatterFactory(new DateFormatter(format)));
JJodaTimeFieldConnector connector = new JJodaTimeFieldConnector(
propertyDescriptor.getName(),
viewComponent);
connector.setExceptionHandler(actionHandler);
adjustSizes(propertyViewDescriptor,
viewComponent,
createFormatter(format, propertyDescriptor),
getDateTemplateValue(propertyDescriptor),
Toolkit.getDefaultToolkit().getScreenResolution() / 3);
return constructView(viewComponent, propertyViewDescriptor, connector);
}
protected IView<JComponent> createLocalDateTimePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
ILocalDateTimePropertyDescriptor propertyDescriptor = (ILocalDateTimePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
JDateField viewComponent = createJDateField(locale);
DateFormat format = createDateFormat(propertyDescriptor, locale);
viewComponent.getFormattedTextField().setFormatterFactory(
new DefaultFormatterFactory(new DateFormatter(format)));
JJodaTimeFieldConnector connector = new JJodaTimeFieldConnector(
propertyDescriptor.getName(),
viewComponent);
connector.setExceptionHandler(actionHandler);
adjustSizes(propertyViewDescriptor,
viewComponent,
createFormatter(format, propertyDescriptor),
getDateTemplateValue(propertyDescriptor),
Toolkit.getDefaultToolkit().getScreenResolution() / 3);
return constructView(viewComponent, propertyViewDescriptor, connector);
}
private TableCellRenderer createDateTableCellRenderer(ILocalDatePropertyDescriptor propertyDescriptor, Locale locale) {
return new FormattedTableCellRenderer(createDateFormatter(propertyDescriptor, locale));
}
private TableCellRenderer createDateTableCellRenderer(ILocalDateTimePropertyDescriptor propertyDescriptor, Locale locale) {
return new FormattedTableCellRenderer(createDateFormatter(propertyDescriptor, locale));
}
private IFormatter createDateFormatter(
ILocalDatePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createDateFormatter(
ILocalDateTimePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createFormatter(DateFormat format, ILocalDatePropertyDescriptor marker) {
return new LocalDateFormatAdapter(format);
}
private IFormatter createFormatter(DateFormat format, ILocalDateTimePropertyDescriptor marker) {
return new LocalDateTimeFormatAdapter(format);
}
private LocalDate getDateTemplateValue(ILocalDatePropertyDescriptor marker) {
return LocalDate.fromDateFields(TEMPLATE_DATE);
}
private LocalDateTime getDateTemplateValue(ILocalDateTimePropertyDescriptor marker) {
return LocalDateTime.fromDateFields(TEMPLATE_DATE);
}
}
package org.popsuite.framework.view.wings;
[...]
/**
* Manages the joda's dates
*/
public class PopsuiteWingsViewFactory
extends DefaultWingsViewFactory{
private static final Date TEMPLATE_DATE = new Date(27166271000L);
@Override
protected IView<SComponent> createPropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
IView<SComponent> view = null;
IPropertyDescriptor propertyDescriptor = (IPropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
if (propertyDescriptor instanceof ILocalDatePropertyDescriptor) { // must come before ILocalDateTimePropertyDescriptor
view = createLocalDatePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
else if (propertyDescriptor instanceof ILocalDateTimePropertyDescriptor) {
view = createLocalDatePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
if (view = = null) {
return super.createPropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
decorateWithDescription(propertyDescriptor, locale, view); // see AbstractViewFactory.createPropertyView
return view;
}
@Override
protected STableCellRenderer createTableCellRenderer(
IPropertyDescriptor propertyDescriptor,
Locale locale) {
if (propertyDescriptor instanceof ILocalDatePropertyDescriptor) { // must come before ILocalDateTimePropertyDescriptor
return createDateTableCellRenderer(
(ILocalDatePropertyDescriptor) propertyDescriptor,
locale);
}
if (propertyDescriptor instanceof ILocalDateTimePropertyDescriptor) {
return createDateTableCellRenderer(
(ILocalDateTimePropertyDescriptor) propertyDescriptor,
locale);
}
return super.createTableCellRenderer(propertyDescriptor, locale);
}
protected IView<SComponent> createLocalDatePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
ILocalDatePropertyDescriptor propertyDescriptor = (ILocalDatePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
XCalendar viewComponent = createDateField();
DateFormat format = createDateFormat(propertyDescriptor, locale);
viewComponent.setFormatter(new SDateFormatter(format));
XCalendarConnector connector = new XJodaTimeConnector(
propertyDescriptor.getName(),
viewComponent);
connector.setExceptionHandler(actionHandler);
adjustSizes(propertyViewDescriptor,
viewComponent,
createFormatter(format, propertyDescriptor),
getDateTemplateValue(propertyDescriptor),
64);
return constructView(viewComponent, propertyViewDescriptor, connector);
}
protected IView<SComponent> createLocalDateTimePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
ILocalDateTimePropertyDescriptor propertyDescriptor = (ILocalDateTimePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
XCalendar viewComponent = createDateField();
DateFormat format = createDateFormat(propertyDescriptor, locale);
viewComponent.setFormatter(new SDateFormatter(format));
XCalendarConnector connector = new XJodaTimeConnector(
propertyDescriptor.getName(),
viewComponent);
connector.setExceptionHandler(actionHandler);
adjustSizes(propertyViewDescriptor,
viewComponent,
createFormatter(format, propertyDescriptor),
getDateTemplateValue(propertyDescriptor),
64);
return constructView(viewComponent, propertyViewDescriptor, connector);
}
private STableCellRenderer createDateTableCellRenderer(ILocalDatePropertyDescriptor propertyDescriptor, Locale locale) {
return new FormattedTableCellRenderer(createDateFormatter(propertyDescriptor, locale));
}
private STableCellRenderer createDateTableCellRenderer(ILocalDateTimePropertyDescriptor propertyDescriptor, Locale locale) {
return new FormattedTableCellRenderer(createDateFormatter(propertyDescriptor, locale));
}
private IFormatter createDateFormatter(
ILocalDatePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createDateFormatter(
ILocalDateTimePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createFormatter(DateFormat format, ILocalDatePropertyDescriptor marker) {
return new LocalDateFormatAdapter(format);
}
private IFormatter createFormatter(DateFormat format, ILocalDateTimePropertyDescriptor marker) {
return new LocalDateTimeFormatAdapter(format);
}
private LocalDate getDateTemplateValue(ILocalDatePropertyDescriptor marker) {
return LocalDate.fromDateFields(TEMPLATE_DATE);
}
private LocalDateTime getDateTemplateValue(ILocalDateTimePropertyDescriptor marker) {
return LocalDateTime.fromDateFields(TEMPLATE_DATE);
}
}
package org.popsuite.framework.view.remote;
[...]
/**
* Manages the joda's dates
*/
public class PopsuiteRemoteViewFactory
extends DefaultRemoteViewFactory {
/**
* {@inheritDoc}
*/
@Override
protected IView<RComponent> createPropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
IView<RComponent> view = null;
IPropertyDescriptor propertyDescriptor = (IPropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
if (propertyDescriptor instanceof ILocalDatePropertyDescriptor) { // must come before ILocalDateTimePropertyDescriptor
view = createLocalDatePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
else if (propertyDescriptor instanceof ILocalDateTimePropertyDescriptor) {
view = createLocalDateTimePropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
if (view = = null) {
return super.createPropertyView(
propertyViewDescriptor,
actionHandler,
locale);
}
decorateWithDescription(propertyDescriptor, locale, view); // see AbstractViewFactory.createPropertyView
if ((view != null) && (propertyDescriptor.getName() != null)) { // see DefaultRemoteViewFactory.createPropertyView
view.getPeer().setLabel(
propertyDescriptor.getI18nName(getTranslationProvider(), locale));
}
return view;
}
protected IView<RComponent> createLocalDatePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
IValueConnector connector;
ILocalDatePropertyDescriptor propertyDescriptor = (ILocalDatePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
if (isDateServerParse()) {
connector = getConnectorFactory().createFormattedValueConnector(
propertyDescriptor.getName(),
createDateFormatter(propertyDescriptor, locale));
} else {
connector = getConnectorFactory().createValueConnector(
propertyDescriptor.getName());
}
connector.setExceptionHandler(actionHandler);
RDateField viewComponent = createRDateField(connector);
viewComponent.setType(propertyDescriptor.getType().toString());
IView<RComponent> view = constructView(viewComponent, propertyViewDescriptor, connector);
return view;
}
protected IView<RComponent> createLocalDateTimePropertyView(
IPropertyViewDescriptor propertyViewDescriptor,
IActionHandler actionHandler,
Locale locale) {
IValueConnector connector;
ILocalDateTimePropertyDescriptor propertyDescriptor = (ILocalDateTimePropertyDescriptor) propertyViewDescriptor.getModelDescriptor();
if (isDateServerParse()) {
connector = getConnectorFactory().createFormattedValueConnector(
propertyDescriptor.getName(),
createDateFormatter(propertyDescriptor, locale));
} else {
connector = getConnectorFactory().createValueConnector(
propertyDescriptor.getName());
}
connector.setExceptionHandler(actionHandler);
RDateField viewComponent = createRDateField(connector);
viewComponent.setType(propertyDescriptor.getType().toString());
IView<RComponent> view = constructView(viewComponent, propertyViewDescriptor, connector);
return view;
}
private IFormatter createDateFormatter(
ILocalDatePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createDateFormatter(
ILocalDateTimePropertyDescriptor propertyDescriptor,
Locale locale) {
return createFormatter(createDateFormat(propertyDescriptor, locale), propertyDescriptor);
}
private IFormatter createFormatter(DateFormat format, ILocalDatePropertyDescriptor marker) {
return new LocalDateFormatAdapter(format);
}
private IFormatter createFormatter(DateFormat format, ILocalDateTimePropertyDescriptor marker) {
return new LocalDateTimeFormatAdapter(format);
}
}
In the project frontend file, in the bean "applicationFrontController", for the attribute "viewFactory", the standard value "viewFactory" must be replaced by a specific one, for example "popsuiteViewFactory".
Then the bean "popsuiteViewFactory" has to be defined in new specific frontend files, one for each of the view factories.
In frontend-swing.xml:
<bean
id="popsuiteViewFactory"
class="org.popsuite.framework.view.swing.PopsuiteSwingViewFactory"
parent="viewFactory"
scope="prototype">
</bean>
In frontend-wings.xml:
<bean
id="popsuiteViewFactory"
class="org.popsuite.framework.view.wings.PopsuiteWingsViewFactory"
parent="viewFactory"
scope="prototype">
</bean>
In frontend-remote.xml:
<bean
id="popsuiteViewFactory"
class="org.popsuite.framework.view.remote.PopsuiteRemoteViewFactory"
parent="viewFactory"
scope="prototype">
<property name="connectorFactory" ref="popsuiteConnectorFactory" />
</bean>
As new frontend configuration files have been created, the project file "beanRefFactoy.xml" must know where to find them. It will be done for each UI context bean, e.g. for Swing (in bold):
<bean
id="popsuite9-swing-context"
class="org.springframework.context.support.ClassPathXmlApplicationContext"
lazy-init="true">
<constructor-arg>
<list>
[...]
<value>org/jspresso/framework/application/frontend/commons-frontend.xml</value>
<value>org/jspresso/framework/application/frontend/swing/commons-swing.xml</value>
[...]
<value>org/popsuite/hr/frontend/frontend.xml</value>
<value>org/popsuite/framework/application/frontend/frontend-swing.xml</value>
[...]
</list>
</constructor-arg>
</bean>
Each new model type needs a new format adapter. But each adaptor is used by all the view factories.
For Joda LocalDateTime:
package org.popsuite.framework.util.format;
import static org.popsuite.framework.model.specialcase.SpecialDates.isUnknown;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.jspresso.framework.util.format.FormatAdapter;
/**
* An adapter for the <code>Format</code> joda objects.
*
* Uses the jdk's Format with joda's objects
*
*
*/
public class LocalDateTimeFormatAdapter extends FormatAdapter {
public LocalDateTimeFormatAdapter(DateFormat format) {
super(format);
}
/**
* {@inheritDoc}
*/
@Override
public String format(Object value) {
if (isUnknown(value)) return null;
Date date;
if (value instanceof LocalDateTime) {
date = ((LocalDateTime) value).toDateTime().toDate();
} else if (value instanceof LocalDate) {
date = ((LocalDate) value).toDateTimeAtStartOfDay().toDate();
} else if (value instanceof Date) {
date = (Date) value; // should never be used but...
} else {
return null;
}
String result = getFormat().format(date);
return result;
}
/**
* {@inheritDoc}
*/
@Override
public Object parse(String dateSource) throws ParseException {
if (null = = dateSource || dateSource.isEmpty()) return null;
Date date = ((DateFormat) getFormat()).parse(dateSource);
return LocalDateTime.fromDateFields(date);
}
}
And for Joda LocalDate:
package org.popsuite.framework.util.format;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import org.joda.time.LocalDate;
/**
* An adapter for the <code>Format</code> joda objects.
*
* Uses the jdk's Format with joda's objects
*
*/
public class LocalDateFormatAdapter extends LocalDateTimeFormatAdapter {
public LocalDateFormatAdapter(DateFormat format) {
super(format);
}
/**
* {@inheritDoc}
*/
@Override
public Object parse(String dateSource) throws ParseException {
if (null = = dateSource || dateSource.isEmpty()) return null;
Date date = ((DateFormat) getFormat()).parse(dateSource);
return LocalDate.fromDateFields(date);
}
}
Not the funiest part of the job, as at the moment there is no way to hook such an extension without changing the template "HibernateXdocletEntity.ftl" itself (°).
So:
- create a copy of the original in your project
- in {project_home}/pom.xml, add:
<properties>
<generator.templateResourcePath>{template_package_path}</generator.templateResourcePath>
</properties>
- add the specific code to the template (see below).
In the template, there is only one place to change. The test on the new date types must be inserted before the test on IDatePropertyDescriptor. The new code is:
<#if !componentDescriptor.computed && !propertyDescriptor.delegateClassName?exists>
* @hibernate.property <#-- Order LocalDate > LocalDateTime > Date is mandatory -->
<#if instanceof(propertyDescriptor, "org.popsuite.framework.model.descriptor.ILocalDatePropertyDescriptor")>
* type = "org.joda.time.contrib.hibernate.PersistentLocalDate"
<#elseif instanceof(propertyDescriptor, "org.popsuite.framework.model.descriptor.ILocalDateTimePropertyDescriptor")>
* type = "org.joda.time.contrib.hibernate.PersistentLocalDateTime"
<#elseif instanceof(propertyDescriptor, "org.jspresso.framework.model.descriptor.IDatePropertyDescriptor")>[...]
That's it.
Indeed, when using a snapshot version of jspresso, you need to check from time to time if there is some change in the original file and to patch them in the project copy.
Note (°) : the FreeMarker template can manage insertion of files. With such a mean, it should be possible to create hooks.
It's the simplest part of the work. In your project:
- check the version of joda library used by default by jspresso, i.e. joda time 1.6+
- add the joda-time-hibernate library, at the moment version 1.1
- change the pom files in the project, i.e.:
* in {project_home}/pom.xml, add:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time-hibernate</artifactId>
<version>1.1</version>
</dependency>
* in {project_home}/core/pom.xml, add (in bold):
<dependencies>
[...]
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time-hibernate</artifactId>
<exclusions>
<exclusion>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</exclusion>
<exclusion>
<groupId>hibernate</groupId>
<artifactId>hibernate</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
</exclusion>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
<exclusion>
<groupId>ehcache</groupId>
<artifactId>ehcache</artifactId>
</exclusion>
<exclusion>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
</exclusion>
<exclusion>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
</exclusion>
</exclusions>
</dependency>
[...]
</dependencies>
[...]
<profiles>
<profile>
<id>ulc</id>
[...]
<build>
<plugins>
[...]
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
[...]
<configuration>
<outputDirectory>${project.build.directory}/${project.artifactId}/lib</outputDirectory>
<includeArtifactIds>ulc-base-trusted,(...],joda-time,joda-time-hibernate,substance</includeArtifactIds>
<stripVersion>true</stripVersion>
</configuration>
[...]
1) Defining the Domain
(model.groovy)
Entity ('User') {
string_32 'login', mandatory:true, unicityScope:'scope_login'
password 'password'
enumeration 'language', enumName:'language', values:['fr', 'en']
set 'roles', ref:'Role', reverse:'Role-users'
}
Entity ('Role') {
string_32 'roleId'
set 'users', ref:'User', composition:false, reverse:'User-roles'
}
2) Update Jaas.config to use the new org.jspresso.framework.security.auth.spi.DatabaseLoginModule (3.5 +). This login module extends the Jboss security DatabaseServerLoginModule, so configuration apply.
(take care to setup java.security.auth.login.config parameter into your tomcat launcher as described in the startup tutorial)
myproject {
org.jspresso.framework.security.auth.spi.DatabaseLoginModule required
dsJndiName="java:comp/env/jdbc/MyProjectDS"
principalsQuery="SELECT PASSWORD, LANGUAGE language FROM USER WHERE LOGIN=?"
rolesQuery="SELECT ROLE.ROLE_ID, 'Roles' FROM ROLE, USER, USER_ROLES \
WHERE USER.LOGIN=? AND USER_ROLES.ROLE_ID=ROLE.ID AND USER_ROLES.USER_ID=USER.ID"
principalClass=org.jspresso.framework.security.UserPrincipal
suspendResume=false;
};
3) For TOMCAT :
Define a jndi name for your database context.xml into tomcat conf directory
<Context>
<Resource name="jdbc/MyProjectDS" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="root" password="pwd" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://10.61.168.88:3306/myproject"/>
</Context>
NB : Take care that tomcat lib contains the jdbc lib library (for example mysql-connector-java-x.x.x.jar )
Setup your datasource project : use the jndi name above
(myproject/webapp/src/main/dev/resources/fr/gefco/myproject/config.xml)
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
default-lazy-init="true">
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/MyProjectDS"/>
</bean>
</beans>
4) For SWING:
4.a) Define the datasource directly into /myproject/core/src/main/resources/fr/gefco/weather/config.xml
<bean
id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource">
<property
name="driverClassName"
value="com.mysql.jdbc.Driver" />
<property
name="url"
value="jdbc:mysql://localhost:3306/myproject" />
<property
name="username"
value="root" />
<property
name="password"
value="pwd" />
</bean>
4.b) Update your SwingApplicationStartup class to setup the jndi name for your database :
@Override
public void start() {
DataSource dataSource = (DataSource) getApplicationContext().getBean("dataSource", DataSource.class);
try {
InitialContext ic = new InitialContext();
Context compSubContext = ic.createSubcontext("java:comp");
Context envSubContext = compSubContext.createSubcontext("env");
Context jdbcSubContext = envSubContext.createSubcontext("jdbc");
jdbcSubContext.rebind("MyProjectDS", dataSource);
} catch (NamingException ex) {
throw new NestedRuntimeException(ex);
}
super.start();
}
4.c) Add a JNDI library to your Jspresso application Launcher.
For my own i'm using Simple-JNDI : you have to add following VM arguments :
-Djava.naming.factory.initial=org.osjava.sj.SimpleContextFactory
-Dorg.osjava.sj.delimiter=/
-Dorg.osjava.sj.jndi.shared=true
-Dorg.osjava.sj.root=conf
5) This step is optional. The Jspresso database login module will store all extra column you put into the principals query (e.g. language in our case) into the session user principal custom properties. However, you might want to keep the user entity instance itself into the application session (also as a custom property). Here is how you would do it using a simple startup action.
a. Add a startup application
(frontend.groovy)
controller (myproject.name', startup:'startupBackAction')
(backend.groovy)
action ('startupBackAction', class:'org.mycompany.myproject.backend.StartupBackAction')
b. Code your startup action
public class StartupBackAction extends AbstractHibernateAction {
public final static String USER_ENTITY_KEY = "session.userEntity";
@SuppressWarnings("unchecked")
@Override
public boolean execute(IActionHandler actionHandler, final Map<String, Object> context) {
final UserPrincipal userPrincipal = getBackendController(context).getApplicationSession().getPrincipal();
User user = (User)getTransactionTemplate(context).execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus txStatus) {
User user = null;
DetachedCriteria userCriteria = DetachedCriteria.forClass(User.class);
userCriteria.add(Restrictions.eq("login", userPrincipal.getName()));
List<User> users = getHibernateTemplate(context).findByCriteria(userCriteria);
if (users!=null && !users.isEmpty()) {
user = users.get(0);
}
txStatus.setRollbackOnly();
return user;
}
});
// merge hibernate instance into session
user = (User)getController(context).merge(user, EMergeMode.MERGE_KEEP);
// set custom property
userPrincipal.putCustomProperty(USER_ENTITY_KEY, user);
return super.execute(actionHandler, context);
}
}
6) Code an administration workspace for User and Role entity
That will not take a while using Jspresso ;)
You should be done !
The model generation is quite long. It's possible to shorten it. On this page, only the quickest options are kept.
So, two things are possible:
- Generate only one GUI, e.g. the Swing one. Even if it's not the final GUI of your project, it's the fastest way to check a prototype.
- Under Eclipse(1): gererate only the java and hbm files.
Disabling a GUI generation can be achieved by commenting modules sections. To keep only the Swing GUI(2), the changes are:
- in file {project}/pom.xml, comment:
<module>webapp</module>
- in file {project}/startup/pom.xml, comment:
<module>wings</module>
<module>remote</module>
<module>flex</module>
<module>qooxdoo</module>
Then launching "mvn package" will generate only the files for Swing(3).
Whenever you would like to disable a UI generation that i based on the webapp (all UIs but swing), you also have to comment out all related dependencies in the webapp module. So, for instance, if you want to disable Qooxdoo UI generation, edit {project}/webapp/pom.xml and comment out all qooxdoo related dependencies.
When working under Eclipse, it's possible to go further. After a first "mvn package", there is no need to do it again, as fas as the changes made to the project concern only the entities and components. Just launch:
- either "mvn generate-resources"(4) if the persistence layer is concerned: java class files and hbm files will be generated;
- otherwise "mvn generate-sources"(5): only the java class files will be generated.
Notes:
(1) the installation of jspresso under Eclipse is detailled here http://www.jspresso.org/page/importing-hr-sample-application-eclipse.
(2) If you are working under Eclipse, to avoid error messages about missing class dependancies, the Web App Libraries must be filled. So, at least one run of "mvn package" with all the web modules is needed.
Besides, when working with a snapshot version of jspresso, the archive files of jspresso duplicated in the project are updated only if the web modules are uncommented.
(3) By the way, you can also disable ddl script generation (that will speed up things a little bit) by disabling the "generate-ddl-script" profile: launch maven with "-P!generate-ddl-scripts" (don't forget the !).
(4) It's possible to launch it from Eclipse. Create a launch configuration for Maven with:
- Base directory: ${project_loc}
- Goals: generate-resources
- Maven Runtime: external, with version 2.2.1
On the "Refresh" tab, check the "Refresh resources upon completion." box with the option "The project containing the selected resource", and the "Recursively include sub-folders" box.
When the configuration is created, you can add it to the Run button menu: click on the Run button and go to "Organize Favorites..."
(5) If the plugin Maven Integration for Eclipse is installed, then it's available from the context menu: Run As/Maven generate-sources. Otherwise, you can proceed as above in note (4).
Jspresso allows you to limit the access to parts of your application to users adhering to a specific role. Jspresso foresees to apply this type of authorization on about all elements of your application: properties, entities, (sub)views, actions,… The Jspresso Reference Guide fully explains this type of authorization.
We could call this type of authorization rules “static authorization”: A user is authorized to a part of the application depending on his role.
In some cases you might need a more dynamic type of authorization. Imagine for instance:
Employees can create and submit holiday requests. Once submitted for approval, the time sheet can no longer be altered.
Clearly here a more dynamic way of authorization is needed: Being authorized to edit an holiday request entity not only depends on the user’s role, but also on the state of the entity. Jspresso supports this kind of dynamic authorization with so-called gates. A gate is open or closed and can be associated to
Every gate refers to a so-called model that determines if the gate is open or closed. Jspresso comes with 2 predefined types of models:
We give here an example of a writabilityGate that is open if the enumeration property state has the value "created" and hence will be closed for other state values (f.i. "submitted"):
<property name="writabilityGates">
<list>
<bean class="org.jspresso.framework.security.GrantedRolesGate">
<property name="grantedRoles">
<list>
<value>Employee</value>
</list>
</property>
</bean>
<bean class="org.jspresso.framework.binding.model.EnumerationPropertyModelGate">
<property ref="accessorFactory" name="accessorFactory" />
<property name="propertyName" value="state" />
<property value="true" name="openOnTrue" />
<property name="openingValues">
<list>
<value>created</value>
</list>
</property>
</bean>
</list>
</property>
For instance, the above writabiligate could be included in an entity holidayRequest provided with an action “submit” that sets its property “state” from "created" to "submitted". Conclusion: The above writabilitygate specifies that only Employees are allowed to edit an holiday request, and only for requests in the state "created".
We invite you to do the test:
1)include the above snippet in model.xml in an entity that has an enumeration property called “state”. Alter the value of "state" and observe the impact. All the views backed by this entity will be impacted.
2/ transfer the above snippet on an entity property descriptor instead of the whole entity. Now only the field (and table cells) backed by this property will have the behaviour (editable <-> read-only) as described above.
3/ now transfer the snippet from the model.xml to the view.xml. Include it in a view on the entity. Only this view will be impacted.
As you could observe, the effect of the writabilityGate will automatically propagate: For instance, if a property is closed for editing, then this will be the case in all views displaying this property. As usal in Jspresso, you must specify things only once!
There are 3 mechanisms to filter the result set of a query on an entity:
This page contains an example of the last variant: IcriteriaRefiner.
The use case goes as follows:
We have an entity holidayRequest. Employees are only allowed to see their own holiday requests. All other user profiles are managers and may see all holiday request.
Furthermore we have other entities that refer to a holidayRequest. We also want that, while selecting the referred holidayRequest, i.e. while picking a holidayRequest via a LOV, the same visibility constraints apply as for querying holidayRequest from the module query panel.
In order to achieve this we just construct two assets:
| Attachment | Size |
|---|---|
| MyEntitytFilter.java_.txt | 1.91 KB |
A common need in projects id to use Maven build profiles in order to customize the produced application for different environments. For instance, you might want to use an in-memory database with schema created and test data inserted at webapp startup in your development environment whereas you will use a "normal" database in production.
This article is about introducing different build profiles to tackle down this problem. Using a standard Jspresso project structure, we will define 2 Maven build profiles, dev an prod. The dev build profile should be the default one, whereas the prod profile should be activated (using -Pprod on the command line) to produce a production-specific, deployable webapp.
The first step is to introduce an environment variable that will have its value assigned depending on the activated profile. Edit your project root pom.xml and add the following section (before the <build> tag for instance) :
<properties>
<environment>dev</environment>
</properties>
<profiles>
<profile>
<id>prod</id>
<properties>
<environment>prod</environment>
</properties>
</profile>
</profiles>
This asssigns "dev" as the value of the environment property, unless the prod profile is activated in the Maven build, in which case the environment property is valued with "prod".
In order to adapt the produced webapp to the targeted platform, we will create 2 extra webapp resource directories :
All other common webapp resources will go in the standard webapp/src/main/resources directory.
For the sake of our example, let's say that we want to :
So we will :
We now have to tell Maven to include the different webapp resources directories based on the activated profile. This is done by adding the following xml fragment in the <build> section of the webapp/pom.xml :
<build>
...
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>${basedir}/src/main/${environment}/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
...
</build>
One common use case consists in customizing the built package depending on the target environement. For instance, you can run your development environment using Tomcat and your pre-production / production environment using JBoss. In that case, there are some libraries that have to be packaged in your development webapp war but not in your production war. Most of the time this is because the target environment provides them as part of their runtime infrastructure and packaging them in the application itself can lead to unexpected results (mainly linked to the classloading architecture). In the example above, JBoss will provide several jars that have to be excluded from the war. This is achieved by marking those dependencies as provided in the prod profile, e.g. :
<build>
...
</build>
<profiles>
<profile>
<id>prod</id>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jboss</groupId>
<artifactId>jbosssx</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jboss</groupId>
<artifactId>jboss-common</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
<profiles>
One last thing we would like to variabilize depending on the targeted environment is the packaged webapp descriptor. For instance, we might want to use different session timeouts for both profiles, disable the insertion of test data on webapp startup for the production war, ...
The standard Jspresso project already contains 2 different web.xml :
In order to further variabilize the used web.xml based on the targeted environment, we will :
We will now tell Maven to package the right web.xml depending on the environment build profile. Edit webapp/pom.xml and look for the 2 <webXml> sections. Replace :
This way, depending on the environment property value, the right web.xml will be picked up for packaging.
Of course, this profile-base build customization can be extended to include different customizations for other domains (e.g. a platform-specific build).
Following our example, everytime you need to create a production artifact, just activate the prod build profile, i.e. :
mvn clean package -Pprod
Securing your Flex/BlazeDS communication requires some configuration.
First of all, a classic architecture :
Flex UI <- HTTPS -> Apache <- HTTP or AJP -> Application Server
Usually, an Apache HTTP server lives in the DMZ and receives HTTPS requests. HTTPS is only necessary over the internet, and the HTTPS payload should be removed from the Apache server before forwarding the (de-crypted) request to the application server servlet engine.
Here are the configuration changes to apply in order to obtain the architecture described above :
<channel-definition id="my-secure-amf" class="mx.messaging.channels.SecureAMFChannel">
<endpoint url="https://{server.name}:{server.port}/{context.root}/messagebroker/amfsecure"
class="flex.messaging.endpoints.SecureAMFEndpoint"/>
<default-channels>
<channel ref="my-amf"/>
<channel ref="my-secure-amf"/>
</default-channels>
<mx:Script>
<![CDATA[
import mx.messaging.ChannelSet;
import mx.messaging.channels.AMFChannel;
import mx.messaging.channels.SecureAMFChannel;
import mx.resources.Locale;
import org.jspresso.framework.application.frontend.controller.flex.DefaultFlexController;
private var flexController:DefaultFlexController;
private function init():void {
var rootURL:String = ExternalInterface.call("getRootURL");
if(rootURL != null) {
var channelSet:ChannelSet = new ChannelSet();
if(rootURL.indexOf("https://") >= 0) {
var secureAmfChannel:SecureAMFChannel =
new SecureAMFChannel("my-secure-amf", rootURL+"/messagebroker/amfsecure");
channelSet.addChannel(secureAmfChannel);
} else {
var amfChannel:AMFChannel = new AMFChannel("my-amf", rootURL+"/messagebroker/amf");
channelSet.addChannel(amfChannel);
}
remoteController.channelSet = channelSet;
}
flexController = new DefaultFlexController(remoteController,
new Locale(resourceManager.localeChain[0]).language);
}
private function start():void {
flexController.start();
}
]]>
</mx:Script>
<script langugage='javascript' type="text/javascript">
function getRootURL()
{
return location.href.substring(0,location.href.indexOf("/flex")); ;
}
function stop()
{
...
}
</script>
Please note that starting from the 3.5.9 version, the archetype already provides the 2 last modifications.
As a side note, there is a common problem you might face if removing the HTTPS payload on the Apache front and forwarding HTTP (not AJP) to the application server using apache mmod_proxy for instance. The Jspresso backend sometimes analyses the incoming request in order to build a new one and send it back to the Flex frontend. This allows for the same archive to be deployed on different infrastructure without being tied to a particular hostname, port or protocol. So far so good. But now, Flex sends HTTPS and the Jspresso backend receives HTTP, so it builds and sends back an HTTP based request to the Flex frontend, which, in turn will then use an un-crypted request. For instance, this is true for dynamically downloaded resources (e.g. images, documents built on the fly, and so on).
In order to fix this problem, the origin protocol (HTTPS) must be restored in the Java object representing the request before being passed to the Jspresso code. This common problem can be solved in Tomcat (and thus JBoss) by installing a custom Tomvcat Valve - org.apache.catalina.connector.RemoteIpValve - that will do the job. For the french speaking readers, there is a very good article from Xebia here that details the whole story. Here is the Tomcat documentation of the valve.
There is a new cute native Jspresso feature : IMPORT CSV FILE TO ANY TABLE


action ref:'importBoxAction'
It is also possible to customize it deeply : for example to manage some additional columns, changing the CSV separator, forcing the encoding, etc. :
action ('importLineOfProductAction',
parent:'importBoxAction',
custom:[ excludedFields:['createdTimestamp', 'updatedTimestamp', 'createdBy', 'updatedBy'],
additionalFields:['owner.lastname', 'owner.firstname', 'owner.userGroup.groupId']
]
)
The source code is available in the following class : org.jspresso.contrib.frontend.ImportEntitiesFactoryBean
This new import fetaure is available today in Jspresso-extension 1.0-SNAPSHOT and will be released in Jspresso-extension 1.0.16.
Enjoy !
The Jspresso Team
This article will explain how to import a Jspresso module.
Step 1 : Declare the maven dependencies and setup xdoclet to generate also entity descriptors for module's components :
pom.xml :
<properties>
<mymodule.version>1.0-SNAPSHOT</mymodule.version>
</properties>
<dependency>
<groupId>com.example.mymodule</groupId>
<artifactId>mymodule-core</artifactId>
<version>${mymodule.version}</version>
</dependency>
core/pom.xml :
<dependency>
<groupId>com.example.mymodule</groupId>
<artifactId>mymodule-core</artifactId>
<version>${mymodule.version}</version>
</dependency>
[...]
<plugin>
<groupId>org.codehaus.xdoclet</groupId>
<artifactId>maven2-xdoclet2-plugin</artifactId>
<executions>
<execution>
<id>xdoclet-hibernate</id>
<phase>generate-resources</phase>
<goals>
<goal>xdoclet</goal>
</goals>
<configuration>
<configs>
<config>
<components>
<component>
<classname>org.xdoclet.plugin.hibernate.HibernateMappingPlugin</classname>
<params>
<version>3.0</version>
<destdir>${project.build.directory}/generated-resources/xdoclet</destdir>
<validate>false</validate>
</params>
</component>
</components>
<includes>**/model/**/*.java,**/model/*.java</includes>
</config>
</configs>
<sourceArtifacts>com.example.mymodule:mymodule-core</sourceArtifacts>
</configuration>
</execution>
</executions>
</plugin>
Step 2 : Complete the Spring configuration to include module's spring configurations files :
For each configuration add the corresponding modules :
beanRefFactory.xml :
<bean
id="myapp-remote-context"
class="org.springframework.context.support.ClassPathXmlApplicationContext"
lazy-init="true">
<constructor-arg>
<list>
<value>org/jspresso/framework/application/commons.xml</value>
<value>org/jspresso/framework/application/backend/persistence/hibernate/commons-hibernate.xml</value>
<value>org/jspresso/framework/application/frontend/commons-frontend.xml</value>
<value>org/jspresso/framework/application/frontend/remote/commons-remote.xml</value>
<!-- my module extensions -->
<value>com/example/mymodule/model/dsl-model.xml</value>
<value>com/example/mymodule/model/model.xml</value>
<value>com/example/mymodule/backend/dsl-backend.xml</value>
<value>com/example/mymodule/backend/backend.xml</value>
<value>com/example/mymodule/view/dsl-view.xml</value>
<value>com/example/mymodule/view/view.xml</value>
<value>com/example/mymodule/frontend/dsl-frontend.xml</value>
<value>com/example/mymodule/frontend/frontend.xml</value>
<!-- my app -->
<value>com/example/myapp/model/dsl-model.xml</value>
<value>com/example/myapp/model/model.xml</value>
<value>com/example/myapp/backend/dsl-backend.xml</value>
<value>com/example/myapp/backend/backend.xml</value>
<value>com/example/myapp/view/dsl-view.xml</value>
<value>com/example/myapp/view/view.xml</value>
<value>com/example/myapp/frontend/dsl-frontend.xml</value>
<value>com/example/myapp/frontend/frontend.xml</value>
<value>com/example/myapp/config.xml</value>
</list>
</constructor-arg>
</bean>
Step 3 : Complete the Hibernate mapping
core-config.xml :
<bean id="hibernateSessionFactory" parent="abstractHibernateSessionFactory">
<property name="hibernateProperties">
<props>
<prop key="hibernate.query.substitutions">true 1, false 0, yes 'Y', no 'N'</prop>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<prop key="hibernate.order_updates">true</prop>
<prop key="hibernate.max_fetch_depth">1</prop>
<prop key="hibernate.default_batch_fetch_size">8</prop>
<prop key="hibernate.jdbc.batch_versioned_data">false</prop>
<prop key="hibernate.jdbc.use_streams_for_binary">true</prop>
<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
<prop key="hibernate.jdbc.batch_size">${hibernate.jdbc.batch_size}</prop>
</props>
</property>
<property name="mappingLocations">
<list>
<value>classpath*:com/example/myapp/**/*.hbm.xml</value>
<value>classpath*:com/example/mymodule/**/*.hbm.xml</value>
</list>
</property>
</bean>
Step 4 : Import jspresso modules into SJS
application.groovy :
ManageModule libraries = new ManageModule()
libraries.importModuleAsResource('mymodule')
def domainBuilder = new Domain(libraries)
Step 5 : Including jspresso module's internationalization keys
frontend.groovy :
messageSource(
basenames:['com.example.myapp', 'com.example.mymodule'])
NB : A future article will explain how to move a standard Jspresso projet to a Jspresso module
Maxime
Jspresso provides all the necessary plumbing to seamlessly integrate any JAAS login module and as of this writing, there are 2 login modules that come with the framework :
• The development login module which does not require any backend and is perfectly suited for development.
• The ldap login module which authenticates the user against an LDAP directory [1].
You may find many other, freely available, JAAS login modules to meet your needs (JDBC, ascii encrypted file, ...).
Here, we will discuss the LDAP login module and its extension to deal with custom properties.
The Jspresso LdapLoginModule extends the JBoss 4.2.2 LdapExtLoginModule so its documentation [2] apply for the basic configuration. It's an enhanced module with support for custom properties to be extracted from he LDAP directory and stored in the Jspresso org.jspresso.framework.security.UserPrincipal (put/getCustomProperty).
The syntax for custom properties configuration is the following :
custom.propertyName="expression"
where expression is in the form :
recordDN[sliceStart, sliceEnd].attribute
some examples :
with slicing :
The same as above, but extracting an attribute instead of the DN :
Notes:
[1] As LDAP tools, see Apache Directory Studio. It works under Eclipse and contains anything you need. You can start with the embedded server and the browser. Even for a newbie like me, with it, LDAP is a breeze.
[2] See also http://www.jboss.org/community/wiki/LdapExtLoginModule
Starting from version 3.5.5, Jspresso uses Hibernate 3.6. When deploying your project under JBoss 4.x you hit Hibernate version conflict errors since JBoss provides older Hibernate jars under the server lib/ directory.
You have two options to resolve this conflict :
Let's dive into the second option. But before going forward, you might fing useful to learn about Java and JBoss classloading. For those of you who read french, you can go through this post.
The problem :
Let's describe the problem more accurately :
- Hibernate libraries are defined as framework/application dependencies, but also defined as JBoss libraries.
- According to the default JBoss classloading your application try to find libraries first in the JBoss libraries and then, in the application libraries.
- So the old, outdated Hibernate libraries get loaded from the JBoss libs, hiding you nice up-to-date ones.
- You get all sort of exceptions...
The solution :
So what to do ? We are going to deploy the application in its own classloader, so that :
How ? The solution is nicely described in this blog post : http://jaitechwriteups.blogspot.com/2008/08/how-to-upgrade-hibernate-in-JBoss.html?m=1
1 - Isolate the application WAR classloader :
Create a file webapp/src/main/webapp/WEB-INF/jboss-web.xml that contains the definition of the classloader :
<?xml version="1.0" encoding="UTF-8"?> |
See the java2ParentDelegation=false ? This is what makes the classloader look first into the application libraries before going to the server libraries. Of course, com.example and yourapp can be any string as long as they are unique.
2 - Make sure that all Hibernate dependencies are actually present in the application libraries. If not, the application classloader is going to look first in its libraries (which is fine) and then in the server libraries. This will be especially problematic for Hibernate, as this is very well explained in the blog post mentioned above.
Here is what we found out to work well for declaring the needed extra Hibernate libraries.
In your root pom.xml in dependencyManagement section:
<dependency> |
and in your webapp/pom.xml :
<dependency> |
This step will bring a compatible hibernate-validator library to the aplication that is only here to hide the application server incompatible one. The tricky part is the hibernate-commons-annotation exclusion. It is there to prevent hibernate-validator to tansitively bring an old incompatible version of hibernate-commons-annotation, thus keeping the one that is compatible with our Hibernate 3.6 version, i.e. the 3.2.0.Final.
With these few updates, your application should run without problem in an unmodified JBoss 4.x server instance.
Whenever you have to package and deploy an EAR to the server, you have to apply almost the same modifications :
1 - Create a file earmodule/src/main/application/META-INF/jboss-app.xml to isolate the EAR classloader.
<?xml version="1.0" encoding="UTF-8"?> |
2 - Modify earmodule/pom.xml to add hibernate dependencies :
<dependency> |
Jspresso is able to deal with different entity realms in order to cleanly synchronize with transactions. An entity, identified by its id, may have at most 1 instance in 1 realm. Each entity realm keeps track of the entity version and dirty state (what are the properties that have been changed on the entity and have not been flushed to the DB). 2 realms cannot share the same object instance for the same entity instance.
For one user application session, there are usually at most 2 entity realms :
When performing DB queries using the Hibernate backend controller method findXXXByCriteria(...), there is, behind the scene a UOW realm. Whenever the call is performed from a pre-existing UOW, then the same realm is used, and no merge is done since it will be performed depending on the outer TX outcome. But whenever ther is no UOW that has already been started, the method starts one, performs the query, and merges the UOW realm entities in the session realm.
When you start a transaction, clone an entity from the session realm to the UOW realm, update the UOW entity and commit the transaction, you intuitively expect the session entity to receive the updated state, its dirty state to be cleared, and its version to be incremented. This is what we call the CLEAN_LAZY merge mode.
Now, what happens when you retrieve fresh entitities from the DB and want to import (merge) them in the session realm ? Do you want to replace the session entity state with the DB state in any case (like a reload operation) ? Do you want to replace the state if and only if the entity version is newer in th DB (i.e. another user committed a new state) and the session entity state is obsolete ? We have to instruct Jspresso what to when merging an entity from one realm to another. Moreover, this merging process is recursive along the entity reachable graph. When merging an entity between 2 realms, you are not only merging the entity itself, but also all the entities that are related to it through the association relationships and recursively.
Jspresso resolves this problem by introducing merge modes that (hopefully) cover the various merging strategies applied for merging one entity instance and its reachable graph. There are 2 big categories of merge modes :
The different supported CLEAN merge modes :
The other merge modes that do not clean traversed entities dirty states :
Don't forget that merging entities serve 2 purposes :
See documentation ref. "I.6.1.2. The backend configuration"
1) Create your database (bellow are sample file for MYSQL)
Take care to disable hibernate schema generation (parameter hibernate.hbm2ddl.auto)
2) Init your database schema :
- launch mvn package
- extract "myproject-schema-mysql-innodb.sql" file from myproject/core/target/weather-core-1.0-SNAPSHOT-ddl-scripts.zip
- execute script on your new DB
3) Change your swing application launcher to avoid TestDataPersister execution :
Replace
-applicationClass fr.gefco.weather.startup.swing.development.SwingDevApplicationStartup
By
-applicationClass fr.gefco.weather.startup.swing.SwingApplicationStartup
That's all !
----------------------
Here is a sample config.xml file for MySQL :
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
default-lazy-init="true">
<!-- dataSource MySql localhost -->
<bean
id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource">
<property
name="driverClassName"
value="com.mysql.jdbc.Driver" />
<property
name="url"
value="jdbc:mysql://localhost:3306/myproject" />
<property
name="username"
value="root" />
<property
name="password"
value="rootpwd" />
</bean>
<!-- HIBERNATE MYSQL -->
<bean
id="hibernateSessionFactory"
parent="abstractHibernateSessionFactory">
<property
name="hibernateProperties">
<props>
<prop
key="hibernate.query.substitutions">true 1, false 0, yes 'Y', no 'N'</prop>
<prop
key="hibernate.dialect">org.jspresso.framework.model.persistence.hibernate.dialect.MySQL5InnoDBDialect</prop>
<prop
key="hibernate.order_updates">true</prop>
<prop
key="hibernate.max_fetch_depth">1</prop>
<prop
key="hibernate.default_batch_fetch_size">8</prop>
<prop
key="hibernate.jdbc.batch_versioned_data">true</prop>
<prop
key="hibernate.jdbc.use_streams_for_binary">true</prop>
<prop
key="hibernate.cache.region_prefix">hibernate.test</prop>
<prop
key="hibernate.cache.provider_class">org.hibernate.cache.HashtableCacheProvider</prop>
<!-- <prop
key="hibernate.hbm2ddl.auto">update</prop> -->
<prop
key="hibernate.jdbc.batch_size">0</prop>
</props>
</property>
<property
name="mappingLocations">
<list>
<value>classpath*:fr/gefco/myproject/**/*.hbm.xml</value>
</list>
</property>
</bean>
</beans>
Jspresso EE supports "Single Sign-on" (SSO) through SPNEGO Integrated Windows Authentication (IWA). This means that you can very easily deploy a Jspresso EE based application and make it participate to your Windows Active Directory SSO architecture using the Kerberos protocol and service tickets exchange.
The main principles of this SSO negociation is described in this excellent OCTO blog post.
Jspresso EE SSO integration relies on the Sourceforge SPNEGO HTTP filter, so all documentation regarding the configuration of this filter apply. Take some time to perform their pre-flight checklist.
Starting from Jspresso EE 3.5.11, the archetype will generate all the necessary stuff for you when creating a new application. However, if you would like to integrate an existing application into your SSO architecture, here are the steps to perform :
Edit core/src/main/resources/your/application/package/frontend/frontend.xml and add the following controller declaration:
<bean id="remoteFrontController" abstract="true" parent="abstractFrontController" |
Edit webapp/pom.xml and add the following dependency :
<dependencies> |
Edit webapp/src/main/webapp/WEB-INF/web.xml and add the following servlet filter declaration :
<web-app> |
In this section we describe the configuration of JBoss 4. Configuring another application server should be as straightforward.
Copy the SPNEGO filter jars and dependencies in your application server instance lib directory, i.e. ${JBOSS_HOME}/server/[your server instance, e.g. default]/lib/ :
All these jars are already present in your webapp but have to be copied in order to be available to the server class-loader. You can retrieve them in webapp/target/[your application]-webapp/WEB-INF/lib :
Create and edit krb5.conf kerberos configuration file :
We suggest to create it under the conf directory of your application server instance, i.e. ${JBOSS_HOME}/server/[your server instance, e.g. default]/conf/. Here is a sample krb5.conf file as directed by the Sourceforge SPNEGO documentation.
[libdefaults] |
Edit the default application server web.xml and add the SPNEGO filter, i.e. ${JBOSS_HOME}/server/[your server instance, e.g. default]/deploy/jboss-web.deployer/conf/web.xml.
This is as directed by the Sourceforge SPNEGO documentation. Note however that the filter type changes since there was a need to make it lenient in order to allow a "classic" login fallback :
<filter> |
Developping a RIA that involves long-lived entities introduces a big challenge regarding transaction handling. In traditional webapps, the transaction is mapped to the ORM session (e.g. JPA or Hibernate) wich, in turn, is mapped to simple and well-defined application lifecycle phases (be it the HTTP request itself with the "open session in view" pattern, or a more extended scope like JBoss Seam or CDI "conversation").
Jspresso offers a more flexible application worlkflow where the user is not driven by pre-defined pages chainings. Unlike in traditional webapps, the transient model state can evolve independently of the persistent state in an arbitrary way until the user decides to save (or rollback) the changes. Therefore, there is a subtle coordination achieved by the framework to that the model in-memory state remains coherent with the persistent one. Jspresso cannot solely rely on the ORM session to handle the session model state since it is bad practice to keep the ORM session open indefinitely and span multiple requests/transactions. Moreover, any runtime exception can make the ORM session unusable, thus invalidating all the transient state.
In order to achieve this, Jspresso implements automatic tracking of session model changes, and seamlessly deals with the ORM to merge those changes back to the persistent store. All this work nicely integrates with transaction management through the concept of "Unit of Work" (referred to as UOW). A Jspresso UOW can be seen as an in-memory draft zone where model parts can be duplicated and modified without impacting the actual model session state. A UOW can be started, committed, rolled-back, very much like transactions indeed. But you will rarely need to manage the UOW yourself since Jspresso automatically maps its lifecycle to the actual underlying transaction platform, e.g. the Hibernate session, the JTA implementor, ... This means that whenever a transaction is actually started, committed or rolled-back, thhe corresponding operations are performed on the UOW.
Jspresso offers built-in standard actions as well as a comprehensive API to handle domain model persistence along with transactions. Once the user as updated parts of the model using the application views and services here is a synopsis of the operations involved in persisting the changes :
There are 2 different ways to start a transaction in Jspresso. The first one is to annotate the backend action class with org.jspresso.framework.application.backend.action.Transactional (beware to use the Jspresso one, not the spring one). It forces the action to execute inside a transaction with a REQUIRED propagation scheme (use the existing transaction if any or start a new one).
@Transactional
public class MyCustomAction extends AbstractHibernateAction {
...
}
The previous method is just a shortcut to using the Spring transaction template that is configured in the application backend controller. This is the second, more versatile way of using transactions in Jspresso :
public class MyCustomAction extends AbstractHibernateAction {
@Override
public boolean execute(IActionHandler actionHandler, final Map<String, Object> context) {
getBackendController(context).getTransactionTemplate().execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus txStatus) {
...
}
});
}
}
The second form allows for more control over the transaction(s) demarcation, e.g. chain 2 or more different transactions in the same action.
Before working on an entity transactionally you have to make a carbon copy of it into the UOW and do all the work on this copy. The backend controller provides the following methods to perform the carbon copy :
IEntity cloneInUnitOfWork(IEntity entity);
List<IEntity> cloneInUnitOfWork(List<IEntity> entities);
Several characteristics of the cloning process :
Note :
You should always make sure that you work on UOW entity clones inside a transaction. This is the only way that Jspresso can garantee the complete isolation of the transaction space. Obscure Hibernate exceptions like "entity with the same identifier was already registered in the session" are generally the consequence of working on entities that have not been cloned into the UOW.
Now that you are in an isolated space, you can do whatever you need without having the risk to polute the application session in case of a failure (either due to a functional violation or a technical error). Note that all operations made on the UOW entities are local to the unit of work, e.g. :
It is time to commit or rollback the transaction. This is automatically done by the Spring transaction template or indirectly at the end of the "execute" method of a Transactional annotated backend action.
Whenever the transaction is rolled back, the UOW is simply discarded and the session state remains untouched.
Whenever the transaction is committed, we have to merge back the UOW state that needs to be propagated to the application session current state.
This last step is automatically performed for created/updated entities whose state has been synchronized (flushed) to the store on transaction commit. During this phase, the entity state is merged back to the correponding session entity (if any) and its dirty state is reset to unmodified.
This is all perfect for save operations; however, there are situations when you need to apply another type of merge from the UOW entities. These other merge modes cannot be automatically inferred by the framework, and thus, must be explicitely coded through the use of the following backend controller methods :
IEntity merge(IEntity entity, EMergeMode mergeMode);
List<IEntity> merge(List<IEntity> entities, EMergeMode mergeMode);
Several characteristics of the merging process :
Jspresso implements 4 different merge modes :