Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

import jakarta.inject.Provider;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import org.springframework.beans.factory.annotation.Autowired;

import org.apache.causeway.applib.annotation.TimePrecision;
import org.apache.causeway.applib.exceptions.recoverable.TextEntryParseException;
import org.apache.causeway.applib.locale.UserLocale;
Expand Down Expand Up @@ -130,6 +132,14 @@ protected String renderHtml(final T value, final Function<T, String> toString) {
.orElseGet(()->getPlaceholderRenderService().asHtml(PlaceholderLiteral.NULL_REPRESENTATION));
}

protected String renderHtml(final T value, final Function<T, String> toString, final UnaryOperator<String> htmlPostprocessor) {
return Optional.ofNullable(value)
.map(toString)
.map(htmlPostprocessor)
.orElseGet(()->getPlaceholderRenderService().asHtml(PlaceholderLiteral.NULL_REPRESENTATION));
}


// -- COMPOSITION UTILS

protected ValueDecomposition decomposeAsString(
Expand Down Expand Up @@ -198,7 +208,6 @@ protected <F> T composeFromNullable(
* this is typically overruled later by implementations of
* {@link #configureDecimalFormat(org.apache.causeway.applib.adapters.ValueSemanticsProvider.Context, DecimalFormat) configureDecimalFormat}
*/
@SuppressWarnings("javadoc")
protected DecimalFormat getNumberFormat(
final ValueSemanticsProvider.@Nullable Context context) {
return getNumberFormat(context, FormatUsageFor.RENDERING);
Expand All @@ -218,9 +227,8 @@ protected Optional<BigInteger> parseInteger(
final ValueSemanticsProvider.@Nullable Context context,
final @Nullable String text) {
var input = _Strings.blankToNullOrTrim(text);
if(input==null) {
if(input==null)
return Optional.empty();
}
try {
return parseDecimal(context, input, GroupingSeparatorPolicy.ALLOW)
.map(BigDecimal::toBigIntegerExact);
Expand All @@ -240,17 +248,15 @@ protected Optional<BigDecimal> parseDecimal(
final @Nullable String text,
final GroupingSeparatorPolicy groupingSeparatorPolicy) {
var input = _Strings.blankToNullOrTrim(text);
if(input==null) {
if(input==null)
return Optional.empty();
}

if (groupingSeparatorPolicy == GroupingSeparatorPolicy.DISALLOW) {
var userLocale = getUserLocale(context);
var decimalFormatSymbols = new DecimalFormatSymbols(userLocale.numberFormatLocale());
var groupingSeparatorChar = decimalFormatSymbols.getGroupingSeparator();
if (input.contains(""+groupingSeparatorChar)) {
if (input.contains(""+groupingSeparatorChar))
throw new TextEntryParseException("Invalid value '" + input + "'; do not use the '" + groupingSeparatorChar + "' grouping separator");
}
}

var format = getNumberFormat(context, FormatUsageFor.PARSING);
Expand All @@ -259,19 +265,17 @@ protected Optional<BigDecimal> parseDecimal(
var position = new ParsePosition(0);
try {
var number = (BigDecimal)format.parse(input, position);
if (position.getErrorIndex() != -1) {
if (position.getErrorIndex() != -1)
throw new ParseException("could not parse input='" + input + "'", position.getErrorIndex());
} else if (position.getIndex() < input.length()) {
else if (position.getIndex() < input.length())
throw new ParseException("input='" + input + "' was not processed completely", position.getIndex());
}
// check for maxFractionDigits if required ...
final int maxFractionDigits = format.getMaximumFractionDigits();
if(maxFractionDigits>-1
&& number.scale()>format.getMaximumFractionDigits()) {
&& number.scale()>format.getMaximumFractionDigits())
throw new TextEntryParseException(String.format(
"No more than %d digits can be entered after the decimal separator, "
+ "got %d in '%s'.", maxFractionDigits, number.scale(), input));
}
return Optional.of(number);
} catch (final NumberFormatException | ParseException e) {
throw new TextEntryParseException(String.format(
Expand Down Expand Up @@ -304,27 +308,17 @@ protected DateTimeFormatter getTemporalNoZoneRenderingFormat(
final @Nullable String datePattern,
final @Nullable String dateTimePattern) {

final DateTimeFormatter noZoneOutputFormat;

switch (temporalCharacteristic) {
case DATE_TIME:
noZoneOutputFormat =
dateTimePattern != null
? DateTimeFormatter.ofPattern(dateTimePattern)
: DateTimeFormatter.ofLocalizedDateTime(dateFormatStyle, timeFormatStyle);
break;
case DATE_ONLY:
noZoneOutputFormat =
datePattern != null
? DateTimeFormatter.ofPattern(datePattern)
: DateTimeFormatter.ofLocalizedDate(dateFormatStyle);
break;
case TIME_ONLY:
noZoneOutputFormat = DateTimeFormatter.ofLocalizedTime(timeFormatStyle);
break;
default:
throw _Exceptions.unmatchedCase(temporalCharacteristic);
}
final DateTimeFormatter noZoneOutputFormat = switch (temporalCharacteristic) {
case DATE_TIME -> dateTimePattern != null
? DateTimeFormatter.ofPattern(dateTimePattern)
: DateTimeFormatter.ofLocalizedDateTime(dateFormatStyle, timeFormatStyle);
case DATE_ONLY -> datePattern != null
? DateTimeFormatter.ofPattern(datePattern)
: DateTimeFormatter.ofLocalizedDate(dateFormatStyle);
case TIME_ONLY -> DateTimeFormatter.ofLocalizedTime(timeFormatStyle);
default -> throw _Exceptions.unmatchedCase(temporalCharacteristic);
};

return noZoneOutputFormat
.withLocale(getUserLocale(context).timeFormatLocale());
}
Expand All @@ -334,16 +328,12 @@ protected Optional<DateTimeFormatter> getTemporalZoneOnlyRenderingFormat(
final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic) {

switch (offsetCharacteristic) {
case LOCAL:
return Optional.empty();
case OFFSET:
return Optional.of(_Temporals.ISO_OFFSET_ONLY_FORMAT);
case ZONED:
return Optional.of(_Temporals.DEFAULT_ZONEID_ONLY_FORMAT);
default:
throw _Exceptions.unmatchedCase(offsetCharacteristic);
}
return switch (offsetCharacteristic) {
case LOCAL -> Optional.empty();
case OFFSET -> Optional.of(_Temporals.ISO_OFFSET_ONLY_FORMAT);
case ZONED -> Optional.of(_Temporals.DEFAULT_ZONEID_ONLY_FORMAT);
default -> throw _Exceptions.unmatchedCase(offsetCharacteristic);
};
}

// -- TEMPORAL FORMATTING/PARSING
Expand All @@ -367,22 +357,18 @@ protected DateTimeFormatter getTemporalIsoFormat(
final TemporalValueSemantics.@NonNull TemporalCharacteristic temporalCharacteristic,
final TemporalValueSemantics.@NonNull OffsetCharacteristic offsetCharacteristic) {

switch (temporalCharacteristic) {
case DATE_TIME:
return offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_DATE_TIME
: DateTimeFormatter.ISO_DATE_TIME;
case DATE_ONLY:
return offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_DATE
: DateTimeFormatter.ISO_DATE;
case TIME_ONLY:
return offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_TIME
: DateTimeFormatter.ISO_TIME;
default:
throw _Exceptions.unmatchedCase(temporalCharacteristic);
}
return switch (temporalCharacteristic) {
case DATE_TIME -> offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_DATE_TIME
: DateTimeFormatter.ISO_DATE_TIME;
case DATE_ONLY -> offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_DATE
: DateTimeFormatter.ISO_DATE;
case TIME_ONLY -> offsetCharacteristic.isLocal()
? DateTimeFormatter.ISO_LOCAL_TIME
: DateTimeFormatter.ISO_TIME;
default -> throw _Exceptions.unmatchedCase(temporalCharacteristic);
};
}

// -- TRANSLATION SUPPORT
Expand All @@ -403,4 +389,22 @@ protected PlaceholderRenderService getPlaceholderRenderService() {
return placeholderRenderService.map(Provider::get).orElseGet(PlaceholderRenderService::fallback);
}

// -- UTILS

/**
* Uses bootstrap CSS.
*/
protected final String toMonospace(final String html) {
return """
<span class="font-monospace fw-light">%s</span>""".formatted(html);
}

/**
* Uses Fontawesome.
*/
protected final String faIconAndTitle(final String faClasses, final String titleHtml) {
return """
<span><i class="%s"></i>%s</span>""".formatted(faClasses, titleHtml);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,31 @@
package org.apache.causeway.core.metamodel.objectmanager.memento;

import java.io.Serializable;
import org.jspecify.annotations.Nullable;

import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.io.JsonUtils;

/**
* Provides a summary of the domain object for rendering,
* having (translated) title and icon.
* having (translated) pre-rendered HTML (typically icon and title).
*
* @implSpec works hand in hand with select2 (third-party) java-script
* and org.apache.causeway.viewer.wicket.ui.components.widgets.select2.Select2 template configuration.
*
* @apiNote SECURITY ADVICE: when using this DTO make sure its content is populated from a trusted source
*/
public record ObjectDisplayDto(
Class<?> correspondingClass,
String bookmark,
String title,
@Nullable String iconHtml) implements Serializable {
String html) implements Serializable {

public static ObjectDisplayDto fromJson(String json) {
public static ObjectDisplayDto fromJson(final String json) {
return JsonUtils.tryRead(ObjectDisplayDto.class, json)
.valueAsNonNullElseFail();
}

public static ObjectDisplayDto fromJsonBase64(String base64EncodedJson) {
public static ObjectDisplayDto fromJsonBase64(final String base64EncodedJson) {
return fromJson(_Strings.base64UrlDecode(base64EncodedJson));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.i18n.TranslationContext;
import org.apache.causeway.applib.services.render.PlaceholderRenderService;
import org.apache.causeway.applib.services.render.PlaceholderRenderService.PlaceholderLiteral;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
Expand All @@ -41,6 +39,7 @@
import org.apache.causeway.core.metamodel.object.MmAssertionUtils;
import org.apache.causeway.core.metamodel.object.MmHintUtils;
import org.apache.causeway.core.metamodel.object.MmTitleUtils;
import org.apache.causeway.core.metamodel.object.MmValueUtils;

/**
* @since 2.0
Expand All @@ -53,7 +52,7 @@ public sealed interface ObjectMemento
Bookmark bookmark();

/**
* The object's title for rendering (before translation).
* The object's title for rendering.
* Corresponds to {@link ManagedObject#getTitle()}.
*
* <p>Directly support choice rendering, without the need to (re-)fetch entire object graphs.
Expand All @@ -64,17 +63,15 @@ public sealed interface ObjectMemento
// -- FACTORIES

static ObjectMemento empty(final LogicalType logicalType) {
return new ObjectMementoEmpty(
logicalType,
PlaceholderRenderService.fallback().asText(PlaceholderLiteral.NULL_REPRESENTATION));
return new ObjectMementoEmpty(logicalType);
}

static Optional<ObjectMemento> singular(
final @Nullable ManagedObject adapter) {
if(ManagedObjects.isNullOrUnspecifiedOrEmpty(adapter))
final @Nullable ManagedObject mo) {
if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mo))
return Optional.empty();

var spec = adapter.objSpec();
var spec = mo.objSpec();

_Assert.assertTrue(spec.isIdentifiable()
|| spec.isParented()
Expand All @@ -86,11 +83,20 @@ static Optional<ObjectMemento> singular(
+ "nor has 'encodable' semantics, nor is (Serializable || Externalizable)"
.formatted(spec));

var title = mo.getTranslationService().translate(TranslationContext.empty(), MmTitleUtils.titleOf(mo));

var prerenderedHtml = spec.isValue()
? "<span>%s</span>".formatted(
MmValueUtils.htmlStringForValueType(null, mo)) // currently only the default value semantics is supported
: "<span>%s %s</span>".formatted(
mo.getObjectRenderService().iconToHtml(mo.getIcon(IconSize.SMALL), IconSize.SMALL),
title);

return Optional.ofNullable(new ObjectMementoSingular(
adapter.logicalType(),
MmHintUtils.bookmarkElseFail(adapter),
adapter.getTranslationService().translate(TranslationContext.empty(), MmTitleUtils.titleOf(adapter)),
adapter.getObjectRenderService().iconToHtml(adapter.getIcon(IconSize.SMALL), IconSize.SMALL)));
mo.logicalType(),
MmHintUtils.bookmarkElseFail(mo),
title,
prerenderedHtml));
}
/**
* returns null for null
Expand Down Expand Up @@ -125,8 +131,8 @@ static ObjectMemento fromDto(final ObjectDisplayDto dto) {
var bookmark = Bookmark.parse(dto.bookmark()).orElseThrow();
var logicalType = new LogicalType(bookmark.logicalTypeName(), dto.correspondingClass());
return bookmark.isEmpty()
? new ObjectMementoEmpty(logicalType, dto.title())
: new ObjectMementoSingular(logicalType, bookmark, dto.title(), dto.iconHtml());
? new ObjectMementoEmpty(logicalType)
: new ObjectMementoSingular(logicalType, bookmark, dto.title(), dto.html());
}

static String enstringToBase64(final ObjectMemento memento) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@

import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.render.PlaceholderRenderService;
import org.apache.causeway.applib.services.render.PlaceholderRenderService.PlaceholderLiteral;

record ObjectMementoEmpty(
LogicalType logicalType,
String title)
LogicalType logicalType)
implements ObjectMemento {

@Override
Expand All @@ -32,7 +33,15 @@ public Bookmark bookmark() {
}

public ObjectDisplayDto toDto() {
return new ObjectDisplayDto(logicalType.correspondingClass(), bookmark().stringify(), title, null);
return new ObjectDisplayDto(logicalType.correspondingClass(), bookmark().stringify(), title(), html());
}

@Override
public String title() {
return PlaceholderRenderService.fallback().asText(PlaceholderLiteral.NULL_REPRESENTATION);
}

private String html() {
return PlaceholderRenderService.fallback().asHtml(PlaceholderLiteral.NULL_REPRESENTATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,18 @@
*/
package org.apache.causeway.core.metamodel.objectmanager.memento;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.bookmark.Bookmark;

record ObjectMementoSingular(
LogicalType logicalType,
Bookmark bookmark,
String title,
@Nullable String iconHtml)
String prerenderedHtml)
implements ObjectMemento {

public ObjectDisplayDto toDto() {
return new ObjectDisplayDto(logicalType.correspondingClass(), bookmark.stringify(), title, iconHtml);
return new ObjectDisplayDto(logicalType.correspondingClass(), bookmark.stringify(), title, prerenderedHtml);
}

@Override public int hashCode() { return bookmark.hashCode(); }
Expand Down
Loading