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 @@ -25,6 +25,7 @@
import org.apache.causeway.applib.mixins.dto.Dto_downloadXml;
import org.apache.causeway.applib.mixins.dto.Dto_downloadXsd;
import org.apache.causeway.applib.mixins.layout.Object_downloadLayout;
import org.apache.causeway.applib.mixins.layout.Object_patchLayout;
import org.apache.causeway.applib.mixins.metamodel.Object_downloadMetamodelXml;
import org.apache.causeway.applib.mixins.metamodel.Object_rebuildMetamodel;
import org.apache.causeway.applib.mixins.rest.Object_openRestApi;
Expand Down Expand Up @@ -111,6 +112,7 @@
Dto_downloadXsd.class,
Object_downloadColumnOrderTxtFilesAsZip.class,
Object_downloadLayout.class,
Object_patchLayout.class,
Object_downloadMetamodelXml.class,
Object_openRestApi.class,
Object_rebuildMetamodel.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.causeway.applib.mixins.layout;

import java.util.Optional;

import jakarta.inject.Inject;

import org.apache.causeway.applib.annotation.Action;
import org.apache.causeway.applib.annotation.ActionLayout;
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.Introspection;
import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.Nature;
import org.apache.causeway.applib.annotation.ParameterLayout;
import org.apache.causeway.applib.annotation.Publishing;
import org.apache.causeway.applib.annotation.RestrictTo;
import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.layout.LayoutConstants;
import org.apache.causeway.applib.layout.resource.LayoutResource;
import org.apache.causeway.applib.services.grid.GridService;
import org.apache.causeway.applib.services.grid.GridService.LayoutKey;
import org.apache.causeway.applib.services.layout.LayoutService;
import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;

import lombok.RequiredArgsConstructor;

/**
* Allows uploading of layout resources, that overrule the default layout resource lookup.
*
* @since 4.0 {@index}
*/
@Action(
domainEvent = Object_patchLayout.ActionDomainEvent.class,
semantics = SemanticsOf.IDEMPOTENT,
commandPublishing = Publishing.DISABLED,
executionPublishing = Publishing.DISABLED,
restrictTo = RestrictTo.PROTOTYPING)
@ActionLayout(
cssClassFa = "solid file-arrow-up",
describedAs = "Uploads layout XML, to be stored in memory for this object type and current layout name. "
+ "It overrules the default layout resource lookup. "
+ "On application restart this information is lost.",
fieldSetId = LayoutConstants.FieldSetId.METADATA,
position = ActionLayout.Position.PANEL_DROPDOWN,
sequence = "700.1.1")
// framework provided domain objects and mixins should explicitly specify their introspection policy
@DomainObject(nature=Nature.MIXIN, mixinMethod = "act", introspection = Introspection.ANNOTATION_REQUIRED)
@RequiredArgsConstructor
public class Object_patchLayout {

public static class ActionDomainEvent
extends org.apache.causeway.applib.CausewayModuleApplib.ActionDomainEvent<Object_patchLayout> {}

@Inject LayoutService layoutService;
@Inject GridService gridService;

private final Object mixee;

@MemberSupport public Object act(
@ParameterLayout(multiLine = 20)
final String layoutXml) {
layoutKey()
.ifPresent(layoutKey->{
gridService.addPatchedLayout(
layoutKey,
new LayoutResource(layoutKey.resourceName(CommonMimeType.XML), CommonMimeType.XML, layoutXml));
});
return mixee;
}

@MemberSupport public boolean hideAct() {
return layoutKey().isEmpty();
}

// -- HELPER

private Optional<LayoutKey> layoutKey() {
return layoutService.layoutKey(mixee);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
import org.apache.causeway.applib.layout.resource.LayoutResource;
import org.apache.causeway.applib.services.layout.LayoutExportStyle;
import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
import org.apache.causeway.commons.internal.base._Strings;
Expand Down Expand Up @@ -58,6 +59,18 @@ public LayoutKey(final Class<?> domainClass) {

public boolean isDefault() { return layoutIfAny==null; }
public boolean isVariant() { return layoutIfAny!=null; }

/**
* Suggested resource name e.g. for export. (not strictly binding)
*/
public String resourceName(CommonMimeType commonMimeType) {
return "%s%s.%s".formatted(
domainClass.getSimpleName(),
isVariant()
? "-" + layoutIfAny()
: "",
commonMimeType.proposedFileExtensions().getFirst().orElse("unknown"));
}
}

/**
Expand All @@ -73,6 +86,17 @@ public LayoutKey(final Class<?> domainClass) {
* <p> Acts as a no-op if not {@link #supportsReloading()}.
*/
void invalidate(Class<?> domainClass);

/**
* Allows to replace or prime layout caches with a custom layout (as provided by given {@link LayoutResource}).
* Useful for prototyping.
*
* <p>This patching is potentially in conflict with layout reloading.
* Patched layouts must stick around even when a layout reload attempt is made.
*
* @since 4.0
*/
void addPatchedLayout(LayoutKey layoutKey, LayoutResource layoutResource);

/**
* Returns a {@link BSGrid} for given {@link LayoutKey}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;

import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -101,6 +102,17 @@ public BSGrid load(final LayoutKey layoutKey) {
.valueAsNonNullElseFail(); // at least we should have a fallback, otherwise there is some serious issue
return grid;
}

@Override
public void addPatchedLayout(LayoutKey layoutKey, LayoutResource layoutResource) {
layoutLookup.addPatchedLayout(layoutKey, layoutResource);
// from 2nd level cache removes the grid that is associated with given layoutKey
cache.gridsByKey().map().remove(layoutKey);
// purge 1st level cache as well
context.specLoaderProvider().get().specForType(layoutKey.domainClass())
.ifPresent(spec->spec.lookupFacet(GridFacet.class)
.ifPresent(GridFacet::clearCache));
}

// -- HELPER

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.causeway.core.metamodel.services.grid;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import org.jspecify.annotations.NonNull;

import org.apache.causeway.applib.layout.resource.LayoutResource;
import org.apache.causeway.applib.services.grid.GridService.LayoutKey;

/**
* Just a wrapper around a concurrent map, that stores patched layout resources.
*/
record LayoutPatchesMap(
/**
* Can be uploaded via UI, for prototyping or troubleshooting.
* Those are meant to overrule the default lookup.
*/
Map<LayoutKey, LayoutResource> layoutPatches) {

public LayoutPatchesMap() {
this(new ConcurrentHashMap<>());
}

public Optional<LayoutResource> lookupPatchedLayoutResource(
@NonNull LayoutKey layoutKey) {
return Optional.ofNullable(layoutPatches.get(layoutKey));
}

public LayoutPatchesMap putPatchedLayoutResource(
@NonNull LayoutKey layoutKey,
@NonNull LayoutResource layoutResource) {
layoutPatches.put(layoutKey, layoutResource);
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,44 @@ record LayoutResourceLookup(
/**
* In effect is used as a Set<LayoutKey>. (there is no concurrent hash set)
*/
Map<LayoutKey, LayoutKey> knownInvalidKeys) {
Map<LayoutKey, LayoutKey> knownInvalidKeys,
/**
* Patched layouts can be uploaded via UI, for prototyping or troubleshooting.
* Those are meant to overrule the default lookup.
*/
LayoutPatchesMap layoutPatchesMap) {

public LayoutResourceLookup(
final Can<LayoutResourceLoader> layoutResourceLoaders) {
this(layoutResourceLoaders, new ConcurrentHashMap<>());
this(layoutResourceLoaders, new ConcurrentHashMap<>(), new LayoutPatchesMap());
}

/**
* if known invalid returns {@link Optional#empty()}
*/
public Optional<LayoutResource> lookupLayoutResource(
Optional<LayoutResource> lookupLayoutResource(
final LayoutKey layoutKey,
final EnumSet<CommonMimeType> supportedFormats) {

if(isKnownInvalid(layoutKey))
return Optional.empty();
if(isKnownInvalid(layoutKey)) {
return Optional.empty();
}

var patchedLayoutResourceOpt = layoutPatchesMap.lookupPatchedLayoutResource(layoutKey);
if(patchedLayoutResourceOpt.isPresent()) {
return patchedLayoutResourceOpt;
}

var layoutResourceOpt = MetaModelContext.instance()
.flatMap(mmc->mmc.specForType(layoutKey.domainClass()))
.flatMap(spec->spec.streamTypeHierarchyAndInterfaces()
.map(ObjectSpecification::getCorrespondingClass)
.flatMap(type->loadContent(type, layoutKey.layoutIfAny(), supportedFormats).stream())
.findFirst());

if(layoutResourceOpt.isPresent())
return layoutResourceOpt;
if(layoutResourceOpt.isPresent()) {
return layoutResourceOpt;
}

log.debug(
"Failed to locate or load layout resource for class {}, "
Expand All @@ -90,19 +102,28 @@ public Optional<LayoutResource> lookupLayoutResource(

return Optional.empty();
}

/**
* Adds given {@link LayoutResource} to the map of patched layouts.
* Patched layouts overrule the default lookup process.
*/
void addPatchedLayout(LayoutKey layoutKey, LayoutResource layoutResource) {
layoutPatchesMap.putPatchedLayoutResource(layoutKey, layoutResource);
unmarkInvalid(layoutKey.domainClass()); // re-evalute validity later on next load
}

boolean isKnownInvalid(final LayoutKey layoutKey) {
return knownInvalidKeys.get(layoutKey)!=null;
}

public void markInvalid(final LayoutKey layoutKey) {
void markInvalid(final LayoutKey layoutKey) {
knownInvalidKeys.put(layoutKey, layoutKey);
}

/**
* To support metamodel invalidation/rebuilding of spec.
*/
public void unmarkInvalid(final Class<?> domainClass) {
void unmarkInvalid(final Class<?> domainClass) {
knownInvalidKeys.entrySet().removeIf(entry->entry.getKey().domainClass().equals(domainClass));
}

Expand Down
Loading
Loading