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 @@ -34,16 +34,20 @@
import io.jmix.flowui.UiComponents;
import io.jmix.flowui.action.entitypicker.EntityClearAction;
import io.jmix.flowui.action.entitypicker.EntityLookupAction;
import io.jmix.flowui.action.multivaluepicker.MultiValueSelectAction;
import io.jmix.flowui.action.valuepicker.ValueClearAction;
import io.jmix.flowui.component.ComponentGenerationContext;
import io.jmix.flowui.component.factory.AbstractComponentGenerationStrategy;
import io.jmix.flowui.component.factory.EntityFieldCreationSupport;
import io.jmix.flowui.component.select.JmixSelect;
import io.jmix.flowui.component.textfield.JmixPasswordField;
import io.jmix.flowui.component.textfield.TypedTextField;
import io.jmix.flowui.component.valuepicker.EntityPicker;
import io.jmix.flowui.component.valuepicker.JmixMultiValuePicker;
import io.jmix.flowui.data.SupportsValueSource;
import io.jmix.flowui.data.ValueSource;
import io.jmix.flowui.data.value.ContainerValueSource;
import io.jmix.flowui.kit.action.Action;
import io.jmix.flowui.kit.component.ComponentUtils;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -84,19 +88,19 @@ public Component createComponent(ComponentGenerationContext context) {
}
}

if (isBoolean(metaProperty)) {
if (metadataTools.isElementCollection(metaProperty)) {
field = createMultiValuePickerField();

} else if (isBoolean(metaProperty)) {
field = createBooleanField();
}

if (isSecret(metaProperty)) {
} else if (isSecret(metaProperty)) {
field = createPasswordField();
}

if (range.isEnum()) {
} else if (range.isEnum()) {
field = createEnumField(range);
}

if (range.isClass()) {
} else if (range.isClass()) {
field = createEntityPickerField();
}

Expand All @@ -109,6 +113,13 @@ public Component createComponent(ComponentGenerationContext context) {
return null;
}

protected JmixMultiValuePicker<?> createMultiValuePickerField() {
JmixMultiValuePicker<?> valuePicker = uiComponents.create(JmixMultiValuePicker.class);
valuePicker.addAction(actions.create(MultiValueSelectAction.ID));
valuePicker.addAction(actions.create(ValueClearAction.ID));
return valuePicker;
}

@SuppressWarnings({"rawtypes", "unchecked"})
protected Select<?> createEnumField(Range range) {
JmixSelect enumField = uiComponents.create(JmixSelect.class);
Expand Down Expand Up @@ -154,7 +165,7 @@ protected boolean isBoolean(MetaProperty metaProperty) {
}

protected boolean requireTextArea(MetaProperty metaProperty, @Nullable Object item) {
if (!String.class.equals(metaProperty.getJavaType())) {
if (!String.class.equals(metaProperty.getJavaType()) || metadataTools.isElementCollection(metaProperty)) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import io.jmix.flowui.util.UnknownOperationResult;
import io.jmix.flowui.view.*;
import io.jmix.flowui.view.navigation.RouteSupport;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Autowired;

import jakarta.persistence.Convert;
Expand Down Expand Up @@ -412,7 +413,7 @@ protected InstanceContainer initInstanceContainerWithDbEntity() {
InstanceContainer container = dataComponents.createInstanceContainer(currentMetaClass.getJavaClass());
entityToEdit = dataManager.load(currentMetaClass.getJavaClass())
.query(String.format(SELECT_APP_SETTINGS_ENTITY_QUERY, currentMetaClass.getName()))
.fetchPlan(fetchPlans.builder(currentMetaClass.getJavaClass()).addFetchPlan(FetchPlan.LOCAL).build())
.fetchPlan(createFetchPlan())
.hint(PersistenceHints.SOFT_DELETION, false)
.optional()
.orElse(null);
Expand All @@ -424,6 +425,16 @@ protected InstanceContainer initInstanceContainerWithDbEntity() {
return container;
}

protected FetchPlan createFetchPlan() {
FetchPlanBuilder builder = fetchPlans.builder(currentMetaClass.getJavaClass()).addFetchPlan(FetchPlan.LOCAL);
for (MetaProperty property : currentMetaClass.getProperties()) {
if (metadataTools.isElementCollection(property)) {
builder.add(property.getName());
}
}
return builder.build();
}

protected boolean isApplicationSettingsEntity(MetaClass metaClass) {
return AppSettingsEntity.class.isAssignableFrom(metaClass.getJavaClass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,15 @@ public class AppSettingsToolsImpl implements AppSettingsTools {
@Autowired
protected AppSettingsProperties appSettingsProperties;

@Autowired
protected FetchPlans fetchPlans;

@Override
public <T extends AppSettingsEntity> T loadAppSettingsEntityFromDataStore(Class<T> clazz) {
//only one record for T can exist at the same time in database with default identifier
return getDataManagerForAppSettingsEntity().load(clazz)
.id(Id.of(1, clazz))
.fetchPlan(createFetchPlan(clazz))
.optional().orElse(metadata.create(clazz, 1));
}

Expand Down Expand Up @@ -114,6 +118,18 @@ protected UnconstrainedDataManager getDataManagerForAppSettingsEntity() {
return appSettingsProperties.isCheckPermissionsForAppSettingsEntity() ? dataManager : unconstrainedDataManager;
}

protected FetchPlan createFetchPlan(Class<?> clazz) {
FetchPlanBuilder builder = fetchPlans.builder(clazz).addFetchPlan(FetchPlan.LOCAL);
for (MetaProperty property : metadata.getClass(clazz).getProperties()) {
if (property.getRange().isClass()) {
builder.add(property.getName(), FetchPlan.BASE);
} else if (metadataTools.isElementCollection(property)) {
builder.add(property.getName());
}
}
return builder.build();
}

@Override
public <T extends AppSettingsEntity> List<String> getPropertyNames(Class<T> clazz) {
return metadata.getClass(clazz).getProperties().stream()
Expand Down
1 change: 1 addition & 0 deletions jmix-core/core/core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
testImplementation 'org.springframework:spring-test'
testImplementation "org.spockframework:spock-core"
testImplementation "org.spockframework:spock-spring"
testImplementation 'org.assertj:assertj-core'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
Expand Down
9 changes: 9 additions & 0 deletions jmix-core/core/src/main/java/io/jmix/core/MetadataTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,15 @@ public boolean isSecret(MetaProperty metaProperty) {
return Boolean.TRUE.equals(metaProperty.getAnnotatedElement().isAnnotationPresent(Secret.class));
}

/**
* Determine whether the given property is an element collection.
* An element collection is a collection of simple types, such as strings, numbers, or dates.
* In a JPA entity, an element collection attribute is annotated with {@code @ElementCollection}.
*/
public boolean isElementCollection(MetaProperty metaProperty) {
return metaProperty.getRange().isDatatype() && metaProperty.getRange().getCardinality().isMany();
}

public Map<String, Object> getMetaAnnotationAttributes(Map<String, Object> metaAnnotations, Class<?> metaAnnotationClass) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) metaAnnotations.get(metaAnnotationClass.getName());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2025 Haulmont.
*
* Licensed 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 io.jmix.core.impl;

import io.jmix.core.EntityInitializer;
import io.jmix.core.JmixOrder;
import io.jmix.core.Metadata;
import io.jmix.core.MetadataTools;
import io.jmix.core.entity.EntityValues;
import io.jmix.core.metamodel.model.MetaClass;
import io.jmix.core.metamodel.model.MetaProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component("core_EntityElementCollectionInitializer")
public class EntityElementCollectionInitializer implements EntityInitializer, Ordered {

private static final Logger log = LoggerFactory.getLogger(EntityElementCollectionInitializer.class);

@Autowired
private Metadata metadata;

@Autowired
private MetadataTools metadataTools;

@Override
public void initEntity(Object entity) {
MetaClass metaClass = metadata.getClass(entity);
for (MetaProperty property : metaClass.getProperties()) {
if (metadataTools.isElementCollection(property)) {
if (List.class.isAssignableFrom(property.getJavaType())) {
EntityValues.setValue(entity, property.getName(), new ArrayList<>());
} else if (Set.class.isAssignableFrom(property.getJavaType())) {
EntityValues.setValue(entity, property.getName(), new HashSet<>());
} else {
log.warn("Unsupported element collection type {} of property {}.{}",
property.getJavaType(), metaClass.getName(), property.getName());
}
}
}
}

@Override
public int getOrder() {
return JmixOrder.HIGHEST_PRECEDENCE + 25;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ protected FetchPlan createKeyValueFetchPlan(KeyValueMetaClass metaClass, String

protected void addAttributesToLocalFetchPlan(MetaClass metaClass, FetchPlanBuilder fetchPlanBuilder) {
for (MetaProperty property : metaClass.getProperties()) {
if (!property.getRange().isClass() && isPersistent(metaClass, property)) {
if (isPersistent(metaClass, property)
&& !property.getRange().isClass()
&& !property.getRange().getCardinality().isMany()) {
fetchPlanBuilder.add(property.getName());
}
}
Expand All @@ -311,7 +313,7 @@ protected void addAttributesToBaseFetchPlan(MetaClass metaClass,
Set<FetchPlanLoader.FetchPlanInfo> visited) {
for (MetaProperty metaProperty : metaClass.getProperties()) {
if (isPersistent(metaClass, metaProperty)) {
if (!metaProperty.getRange().isClass()) {
if (!metaProperty.getRange().isClass() && !metaProperty.getRange().getCardinality().isMany()) {
fetchPlanBuilder.add(metaProperty.getName());
} else if (metaProperty.getType() == MetaProperty.Type.EMBEDDED) {
addClassAttributeWithFetchPlan(metaProperty, FetchPlan.BASE, fetchPlanBuilder, info, visited);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,10 @@ protected boolean isMandatory(AccessibleObject base) {

protected Range.Cardinality getCardinality(Field field) {
if (field.isAnnotationPresent(Column.class)) {
return Range.Cardinality.NONE;
if (field.isAnnotationPresent(ElementCollection.class))
return Range.Cardinality.ONE_TO_MANY;
else
return Range.Cardinality.NONE;
} else if (field.isAnnotationPresent(OneToOne.class)) {
return Range.Cardinality.ONE_TO_ONE;
} else if (field.isAnnotationPresent(OneToMany.class)) {
Expand Down Expand Up @@ -740,7 +743,8 @@ protected boolean hasJpaAnnotation(AnnotatedElement annotatedElement) {
|| annotatedElement.isAnnotationPresent(ManyToMany.class)
|| annotatedElement.isAnnotationPresent(OneToOne.class)
|| annotatedElement.isAnnotationPresent(Embedded.class)
|| annotatedElement.isAnnotationPresent(EmbeddedId.class);
|| annotatedElement.isAnnotationPresent(EmbeddedId.class)
|| annotatedElement.isAnnotationPresent(ElementCollection.class);
}

protected boolean hasJpaAnnotation(Class<?> javaClass) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2025 Haulmont.
*
* Licensed 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 element_collection;

import io.jmix.core.CoreConfiguration;
import io.jmix.core.Metadata;
import io.jmix.core.MetadataTools;
import io.jmix.core.metamodel.model.MetaClass;
import io.jmix.core.metamodel.model.MetaProperty;
import io.jmix.core.metamodel.model.Range;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import test_support.addon1.TestAddon1Configuration;
import test_support.app.TestAppConfiguration;
import test_support.app.entity.element_collection.EcAlpha;

import java.util.List;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {CoreConfiguration.class, TestAddon1Configuration.class, TestAppConfiguration.class})
public class ElementCollectionTest {

@Autowired
Metadata metadata;
@Autowired
MetadataTools metadataTools;

@Test
void testMetadata() {
MetaClass metaClass = metadata.getClass(EcAlpha.class);

MetaProperty property = metaClass.findProperty("tags");
assertThat(property).isNotNull();

MetaProperty.Type propertyType = property.getType();
assertThat(propertyType).isEqualTo(MetaProperty.Type.DATATYPE);

Range propertyRange = property.getRange();
assertThat(propertyRange.isDatatype()).isTrue();

Range.Cardinality cardinality = propertyRange.getCardinality();
assertThat(cardinality.isMany()).isTrue();

assertThat(propertyRange.isOrdered()).isTrue();

assertThat(metadataTools.isJpa(property)).isTrue();
}

@Test
void testInitialization() {
EcAlpha alpha = metadata.create(EcAlpha.class);

assertThat(alpha.getTags()).isNotNull();
assertThat(alpha.getTags()).isInstanceOf(List.class);
assertThat(alpha.getTags()).isEmpty();

assertThat(alpha.getNumbers()).isNotNull();
assertThat(alpha.getNumbers()).isInstanceOf(Set.class);
assertThat(alpha.getNumbers()).isEmpty();
}
}
Loading