diff --git a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/AbstractCreatingOrUpdatingContentCommand.java b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/AbstractCreatingOrUpdatingContentCommand.java index 4f74db24f6d..ded9d17faef 100644 --- a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/AbstractCreatingOrUpdatingContentCommand.java +++ b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/AbstractCreatingOrUpdatingContentCommand.java @@ -1,21 +1,38 @@ package com.enonic.xp.core.impl.content; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.net.MediaType; +import com.enonic.xp.app.ApplicationKey; +import com.enonic.xp.app.ApplicationKeys; +import com.enonic.xp.app.ApplicationWildcardMatcher; import com.enonic.xp.attachment.CreateAttachment; import com.enonic.xp.attachment.CreateAttachments; -import com.enonic.xp.content.processor.ContentProcessor; import com.enonic.xp.content.ContentValidator; +import com.enonic.xp.content.ExtraData; +import com.enonic.xp.content.processor.ContentProcessor; import com.enonic.xp.context.Context; import com.enonic.xp.context.ContextAccessor; import com.enonic.xp.core.internal.FileNames; +import com.enonic.xp.data.PropertyTree; +import com.enonic.xp.form.FormDefaultValuesProcessor; +import com.enonic.xp.schema.content.ContentType; +import com.enonic.xp.schema.content.ContentTypeName; +import com.enonic.xp.schema.content.GetContentTypeParams; +import com.enonic.xp.schema.xdata.XData; import com.enonic.xp.schema.xdata.XDataService; import com.enonic.xp.security.User; import com.enonic.xp.site.SiteService; +import com.enonic.xp.site.XDataMapping; + +import static com.google.common.base.Strings.nullToEmpty; class AbstractCreatingOrUpdatingContentCommand extends AbstractContentCommand @@ -37,6 +54,8 @@ class AbstractCreatingOrUpdatingContentCommand final boolean allowUnsafeAttachmentNames; + final FormDefaultValuesProcessor formDefaultValuesProcessor; + AbstractCreatingOrUpdatingContentCommand( final Builder builder ) { super( builder ); @@ -45,20 +64,23 @@ class AbstractCreatingOrUpdatingContentCommand this.contentProcessors = List.copyOf( builder.contentProcessors ); this.contentValidators = List.copyOf( builder.contentValidators ); this.allowUnsafeAttachmentNames = builder.allowUnsafeAttachmentNames; + this.formDefaultValuesProcessor = builder.formDefaultValuesProcessor; } public static class Builder> extends AbstractContentCommand.Builder { - private XDataService xDataService; + XDataService xDataService; + + SiteService siteService; - private SiteService siteService; + List contentProcessors = List.of(); - private List contentProcessors = List.of(); + List contentValidators = List.of(); - private List contentValidators = List.of(); + boolean allowUnsafeAttachmentNames; - private boolean allowUnsafeAttachmentNames; + FormDefaultValuesProcessor formDefaultValuesProcessor; Builder() { @@ -108,6 +130,13 @@ B allowUnsafeAttachmentNames( final boolean allowUnsafeAttachmentNames ) return (B) this; } + @SuppressWarnings("unchecked") + B formDefaultValuesProcessor( final FormDefaultValuesProcessor formDefaultValuesProcessor ) + { + this.formDefaultValuesProcessor = formDefaultValuesProcessor; + return (B) this; + } + @Override void validate() { @@ -154,6 +183,47 @@ private boolean isExecutableFileName( final String fileName ) return fileName.endsWith( ".exe" ) || fileName.endsWith( ".msi" ) || fileName.endsWith( ".dmg" ) || fileName.endsWith( ".bat" ) || fileName.endsWith( ".sh" ); } + + protected Set getDefaultExtraDatas( final ApplicationKeys applicationKeys, final ContentTypeName contentTypeName ) + { + final ContentType contentType = this.contentTypeService.getByName( GetContentTypeParams.from( contentTypeName ) ); + + Set result = new HashSet<>(); + + result.addAll( + xDataService.getByNames( contentType.getXData() ).stream().map( this::createExtraData ).collect( Collectors.toSet() ) ); + result.addAll( Objects.requireNonNullElse( applicationKeys, ApplicationKeys.empty() ) + .stream() + .map( siteService::getDescriptor ) + .filter( Objects::nonNull ) + .flatMap( siteDescriptor -> siteDescriptor.getXDataMappings() + .stream() + .filter( xDataMapping -> doFilterXDataMapping( xDataMapping, contentTypeName ) ) + .map( this::createExtraData ) ) + .collect( Collectors.toList() ) ); + + return result; + } + + private boolean doFilterXDataMapping( final XDataMapping xDataMapping, final ContentTypeName contentTypeName ) + { + String wildcard = xDataMapping.getAllowContentTypes(); + ApplicationKey applicationKey = xDataMapping.getXDataName().getApplicationKey(); + return nullToEmpty( wildcard ).isBlank() || + new ApplicationWildcardMatcher<>( applicationKey, ContentTypeName::toString, ApplicationWildcardMatcher.Mode.MATCH ).matches( + wildcard, contentTypeName); + } + + private ExtraData createExtraData( final XDataMapping xDataMapping ) + { + XData xData = xDataService.getByName( xDataMapping.getXDataName() ); + return createExtraData( xData ); + } + + private ExtraData createExtraData( final XData xData ) + { + return new ExtraData( xData.getName(), new PropertyTree() ); + } } diff --git a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/ContentServiceImpl.java b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/ContentServiceImpl.java index d376a063c1a..c79c6f63b8c 100644 --- a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/ContentServiceImpl.java +++ b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/ContentServiceImpl.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.InputStream; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -20,6 +21,7 @@ import com.google.common.io.ByteSource; import com.enonic.xp.app.ApplicationKey; +import com.enonic.xp.app.ApplicationKeys; import com.enonic.xp.archive.ArchiveContentParams; import com.enonic.xp.archive.ArchiveContentsResult; import com.enonic.xp.archive.RestoreContentParams; @@ -116,6 +118,8 @@ import com.enonic.xp.node.ReorderChildNodesResult; import com.enonic.xp.node.SetNodeChildOrderParams; import com.enonic.xp.page.PageDescriptorService; +import com.enonic.xp.project.Project; +import com.enonic.xp.project.ProjectName; import com.enonic.xp.project.ProjectService; import com.enonic.xp.query.expr.CompareExpr; import com.enonic.xp.query.expr.FieldExpr; @@ -123,18 +127,22 @@ import com.enonic.xp.query.expr.ValueExpr; import com.enonic.xp.region.LayoutDescriptorService; import com.enonic.xp.region.PartDescriptorService; +import com.enonic.xp.repository.RepositoryId; import com.enonic.xp.schema.content.ContentTypeName; import com.enonic.xp.schema.content.ContentTypeService; import com.enonic.xp.schema.xdata.XDataService; import com.enonic.xp.security.acl.AccessControlList; import com.enonic.xp.site.CreateSiteParams; import com.enonic.xp.site.Site; +import com.enonic.xp.site.SiteConfig; import com.enonic.xp.site.SiteConfigsDataSerializer; import com.enonic.xp.site.SiteService; import com.enonic.xp.trace.Trace; import com.enonic.xp.trace.Tracer; import com.enonic.xp.util.BinaryReference; +import static java.util.stream.Collectors.toList; + @Component(configurationPid = "com.enonic.xp.content") public class ContentServiceImpl implements ContentService @@ -159,6 +167,8 @@ public class ContentServiceImpl private SiteService siteService; + private ProjectService projectService; + private final ContentNodeTranslator translator; private final List contentProcessors = new CopyOnWriteArrayList<>(); @@ -240,6 +250,7 @@ public Site create( final CreateSiteParams params ) contentDataSerializer( this.contentDataSerializer ). allowUnsafeAttachmentNames( config.attachments_allowUnsafeNames() ). params( createContentParams ). + applicationKeys( getApplicationKeysRelatedWithSiteOrProject( params.getParentContentPath() ) ). build(). execute(); @@ -280,6 +291,7 @@ public Content create( final CreateContentParams params ) contentDataSerializer( this.contentDataSerializer ). allowUnsafeAttachmentNames( config.attachments_allowUnsafeNames() ). params( params ). + applicationKeys( getApplicationKeysRelatedWithSiteOrProject( params.getParent() ) ). build(). execute(); @@ -1339,6 +1351,29 @@ private static void verifyContextBranch( final Branch branch ) } } + private ApplicationKeys getApplicationKeysRelatedWithSiteOrProject( final ContentPath parentPath ) + { + Site nearestSite = findNearestSiteByPath( parentPath ); + + List applicationKeys = new ArrayList<>(); + + if ( nearestSite != null ) + { + applicationKeys.addAll( nearestSite.getSiteConfigs().stream().map( SiteConfig::getApplicationKey ).collect( toList() ) ); + } + else + { + RepositoryId repositoryId = ContextAccessor.current().getRepositoryId(); + if ( repositoryId != null ) + { + Project project = projectService.get( ProjectName.from( repositoryId ) ); + applicationKeys.addAll( project.getSiteConfigs().getApplicationKeys() ); + } + } + + return applicationKeys.isEmpty() ? ApplicationKeys.empty() : ApplicationKeys.from( applicationKeys ); + } + @Reference public void setContentTypeService( final ContentTypeService contentTypeService ) { @@ -1407,8 +1442,7 @@ public void setContentAuditLogSupport( final ContentAuditLogSupport contentAudit @Reference public void setProjectService( final ProjectService projectService ) { - //Many starters depend on ContentService available only when default cms repo is fully initialized. - // Starting from 7.3 Initialization happens in ProjectService, so we need a dependency. + this.projectService = projectService; } } diff --git a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateContentCommand.java b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateContentCommand.java index 9ab0c40a7e9..41e9dea6447 100644 --- a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateContentCommand.java +++ b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateContentCommand.java @@ -1,13 +1,16 @@ package com.enonic.xp.core.impl.content; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; +import com.enonic.xp.app.ApplicationKeys; import com.enonic.xp.content.Content; import com.enonic.xp.content.ContentAccessException; import com.enonic.xp.content.ContentAlreadyExistsException; @@ -19,6 +22,8 @@ import com.enonic.xp.content.ContentPropertyNames; import com.enonic.xp.content.CreateContentParams; import com.enonic.xp.content.CreateContentTranslatorParams; +import com.enonic.xp.content.ExtraData; +import com.enonic.xp.content.ExtraDatas; import com.enonic.xp.content.ValidationErrors; import com.enonic.xp.content.processor.ContentProcessor; import com.enonic.xp.content.processor.ProcessCreateParams; @@ -27,7 +32,6 @@ import com.enonic.xp.core.impl.content.serializer.ContentDataSerializer; import com.enonic.xp.core.impl.content.validate.InputValidator; import com.enonic.xp.data.Property; -import com.enonic.xp.form.FormDefaultValuesProcessor; import com.enonic.xp.inputtype.InputTypes; import com.enonic.xp.media.MediaInfo; import com.enonic.xp.name.NamePrettyfier; @@ -41,6 +45,7 @@ import com.enonic.xp.region.PartDescriptorService; import com.enonic.xp.schema.content.ContentType; import com.enonic.xp.schema.content.GetContentTypeParams; +import com.enonic.xp.schema.xdata.XData; import com.enonic.xp.security.PrincipalKey; import com.enonic.xp.security.auth.AuthenticationInfo; @@ -55,8 +60,6 @@ final class CreateContentCommand private final MediaInfo mediaInfo; - private final FormDefaultValuesProcessor formDefaultValuesProcessor; - private final PageDescriptorService pageDescriptorService; private final PartDescriptorService partDescriptorService; @@ -68,13 +71,23 @@ final class CreateContentCommand private CreateContentCommand( final Builder builder ) { super( builder ); - this.params = builder.params; this.mediaInfo = builder.mediaInfo; - this.formDefaultValuesProcessor = builder.formDefaultValuesProcessor; this.pageDescriptorService = builder.pageDescriptorService; this.partDescriptorService = builder.partDescriptorService; this.layoutDescriptorService = builder.layoutDescriptorService; this.contentDataSerializer = builder.contentDataSerializer; + + final Set extraDataSet = new HashSet<>(); + if ( builder.params.getExtraDatas() != null ) + { + extraDataSet.addAll( builder.params.getExtraDatas().getSet() ); + } + if ( builder.applicationKeys != null ) + { + extraDataSet.addAll( getDefaultExtraDatas( builder.applicationKeys, builder.params.getType() ) ); + } + + this.params = CreateContentParams.create( builder.params ).extraDatas( ExtraDatas.from( extraDataSet ) ).build(); } static Builder create() @@ -98,7 +111,17 @@ private Content doExecute() validateContentType( contentType ); formDefaultValuesProcessor.setDefaultValues( contentType.getForm(), params.getData() ); - // TODO apply default values to xData + + if ( params.getExtraDatas() != null ) + { + params.getExtraDatas().forEach( extraData -> { + XData xData = xDataService.getByName( extraData.getName() ); + if ( xData != null ) + { + formDefaultValuesProcessor.setDefaultValues( xData.getForm(), extraData.getData() ); + } + } ); + } CreateContentParams processedParams = runContentProcessors( this.params, contentType ); @@ -325,7 +348,7 @@ static class Builder private MediaInfo mediaInfo; - private FormDefaultValuesProcessor formDefaultValuesProcessor; + private ApplicationKeys applicationKeys; private PageDescriptorService pageDescriptorService; @@ -356,12 +379,6 @@ Builder mediaInfo( final MediaInfo value ) return this; } - Builder formDefaultValuesProcessor( final FormDefaultValuesProcessor formDefaultValuesProcessor ) - { - this.formDefaultValuesProcessor = formDefaultValuesProcessor; - return this; - } - Builder pageDescriptorService( final PageDescriptorService value ) { this.pageDescriptorService = value; @@ -386,6 +403,12 @@ Builder contentDataSerializer( final ContentDataSerializer value ) return this; } + Builder applicationKeys( final ApplicationKeys applicationKeys ) + { + this.applicationKeys = applicationKeys; + return this; + } + @Override void validate() { diff --git a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateMediaCommand.java b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateMediaCommand.java index ffd58470c09..74eeaf78d00 100644 --- a/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateMediaCommand.java +++ b/modules/core/core-content/src/main/java/com/enonic/xp/core/impl/content/CreateMediaCommand.java @@ -11,7 +11,6 @@ import com.enonic.xp.content.CreateMediaParams; import com.enonic.xp.core.impl.content.serializer.ContentDataSerializer; import com.enonic.xp.data.PropertyTree; -import com.enonic.xp.form.FormDefaultValuesProcessor; import com.enonic.xp.media.MediaInfo; import com.enonic.xp.media.MediaInfoService; import com.enonic.xp.page.PageDescriptorService; @@ -27,8 +26,6 @@ final class CreateMediaCommand private final MediaInfoService mediaInfoService; - private final FormDefaultValuesProcessor formDefaultValuesProcessor; - private final PageDescriptorService pageDescriptorService; private final PartDescriptorService partDescriptorService; @@ -42,7 +39,6 @@ private CreateMediaCommand( final Builder builder ) super( builder ); this.params = builder.params; this.mediaInfoService = builder.mediaInfoService; - this.formDefaultValuesProcessor = builder.formDefaultValuesProcessor; this.pageDescriptorService = builder.pageDescriptorService; this.partDescriptorService = builder.partDescriptorService; this.layoutDescriptorService = builder.layoutDescriptorService; @@ -143,8 +139,6 @@ public static class Builder private MediaInfoService mediaInfoService; - private FormDefaultValuesProcessor formDefaultValuesProcessor; - private PageDescriptorService pageDescriptorService; private PartDescriptorService partDescriptorService; @@ -165,12 +159,6 @@ public Builder mediaInfoService( final MediaInfoService value ) return this; } - public Builder formDefaultValuesProcessor( final FormDefaultValuesProcessor formDefaultValuesProcessor ) - { - this.formDefaultValuesProcessor = formDefaultValuesProcessor; - return this; - } - Builder pageDescriptorService( final PageDescriptorService value ) { this.pageDescriptorService = value; diff --git a/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/CreateContentCommandTest.java b/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/CreateContentCommandTest.java index b60fac5cf5a..fb5dccb14d4 100644 --- a/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/CreateContentCommandTest.java +++ b/modules/core/core-content/src/test/java/com/enonic/xp/core/impl/content/CreateContentCommandTest.java @@ -10,6 +10,8 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; +import com.enonic.xp.app.ApplicationKey; +import com.enonic.xp.app.ApplicationKeys; import com.enonic.xp.content.Content; import com.enonic.xp.content.ContentConstants; import com.enonic.xp.content.ContentInheritType; @@ -17,12 +19,18 @@ import com.enonic.xp.content.ContentPath; import com.enonic.xp.content.ContentPropertyNames; import com.enonic.xp.content.CreateContentParams; +import com.enonic.xp.content.ExtraData; +import com.enonic.xp.content.ExtraDatas; import com.enonic.xp.core.impl.content.serializer.ContentDataSerializer; import com.enonic.xp.core.impl.schema.content.BuiltinContentTypesAccessor; import com.enonic.xp.data.PropertySet; import com.enonic.xp.data.PropertyTree; import com.enonic.xp.event.EventPublisher; +import com.enonic.xp.form.Input; import com.enonic.xp.index.ChildOrder; +import com.enonic.xp.inputtype.InputTypeDefault; +import com.enonic.xp.inputtype.InputTypeName; +import com.enonic.xp.inputtype.InputTypeProperty; import com.enonic.xp.media.MediaInfo; import com.enonic.xp.node.CreateNodeParams; import com.enonic.xp.node.Node; @@ -38,11 +46,18 @@ import com.enonic.xp.schema.content.ContentTypeName; import com.enonic.xp.schema.content.ContentTypeService; import com.enonic.xp.schema.content.GetContentTypeParams; +import com.enonic.xp.schema.xdata.XData; +import com.enonic.xp.schema.xdata.XDataName; +import com.enonic.xp.schema.xdata.XDataNames; import com.enonic.xp.schema.xdata.XDataService; +import com.enonic.xp.schema.xdata.XDatas; import com.enonic.xp.security.PrincipalKey; import com.enonic.xp.security.acl.AccessControlEntry; import com.enonic.xp.security.acl.AccessControlList; +import com.enonic.xp.site.SiteDescriptor; import com.enonic.xp.site.SiteService; +import com.enonic.xp.site.XDataMapping; +import com.enonic.xp.site.XDataMappings; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -575,6 +590,119 @@ public void createPageTemplateNotUnderTemplateFolder() assertThrows( IllegalArgumentException.class, command::execute ); } + @Test + public void testCreateCommandWithDefaultExtraDatas() + { + final ApplicationKey applicationKey = ApplicationKey.from( "com.enonic.app.test" ); + final XDataName xDataName = XDataName.from( "com.enonic.app.test:xDataName" ); + final XData xData = XData.create() + .name( xDataName ) + .addFormItem( Input.create() + .label( "Label" ) + .name( "name" ) + .inputType( InputTypeName.CHECK_BOX ) + .defaultValue( + InputTypeDefault.create().property( InputTypeProperty.create( "default", "check" ).build() ).build() ) + .occurrences( 1, 1 ) + .build() ) + .build(); + + mockContentRootNode( null ); + + Mockito.when( contentTypeService.getByName( Mockito.isA( GetContentTypeParams.class ) ) ) + .thenReturn( ContentType.create() + .superType( ContentTypeName.unstructured() ) + .name( ContentTypeName.unstructured() ) + .xData( XDataNames.from( xDataName ) ) + .build() ); + + Mockito.when( xDataService.getByNames( Mockito.any( XDataNames.class ) ) ).thenReturn( XDatas.from( xData ) ); + + Mockito.when( xDataService.getByName( Mockito.any( XDataName.class ) ) ).thenReturn( xData ); + + Mockito.when( siteService.getDescriptor( applicationKey ) ) + .thenReturn( SiteDescriptor.create() + .applicationKey( applicationKey ) + .xDataMappings( XDataMappings.create() + .add( XDataMapping.create() + .xDataName( xDataName ) + .optional( true ) + .allowContentTypes( "^(?!base:folder$).*" ) + .build() ) + .build() ) + .build() ); + + final CreateContentParams params = CreateContentParams.create() + .type( ContentTypeName.unstructured() ) + .name( "name" ) + .parent( ContentPath.ROOT ) + .contentData( new PropertyTree() ) + .displayName( "displayName" ) + .build(); + + final CreateContentCommand command = CreateContentCommand.create() + .params( params ) + .contentTypeService( this.contentTypeService ) + .nodeService( this.nodeService ) + .translator( this.translator ) + .eventPublisher( this.eventPublisher ) + .xDataService( this.xDataService ) + .siteService( this.siteService ) + .pageDescriptorService( this.pageDescriptorService ) + .contentDataSerializer( contentDataSerializer ) + .formDefaultValuesProcessor( ( form, data ) -> { + } ) + .applicationKeys( ApplicationKeys.from( applicationKey ) ) + .build(); + + final Content createdContent = command.execute(); + assertNotNull( createdContent ); + } + + @Test + public void testCreateCommandWithWithExtraDatas() + { + final XDataName xDataName = XDataName.from( "com.enonic.app.test:xDataName" ); + + Mockito.when( contentTypeService.getByName( Mockito.isA( GetContentTypeParams.class ) ) ) + .thenReturn( ContentType.create() + .superType( ContentTypeName.unstructured() ) + .name( ContentTypeName.unstructured() ) + .xData( XDataNames.from( xDataName ) ) + .build() ); + + mockContentRootNode( null ); + + final CreateContentParams params = CreateContentParams.create() + .type( ContentTypeName.unstructured() ) + .name( "name" ) + .parent( ContentPath.ROOT ) + .contentData( new PropertyTree() ) + .displayName( "displayName" ) + .extraDatas( ExtraDatas.create(). + add( new ExtraData( xDataName, new PropertyTree() ) ) + .build() ) + .build(); + + final CreateContentCommand command = CreateContentCommand.create() + .params( params ) + .contentTypeService( this.contentTypeService ) + .nodeService( this.nodeService ) + .translator( this.translator ) + .eventPublisher( this.eventPublisher ) + .xDataService( this.xDataService ) + .siteService( this.siteService ) + .pageDescriptorService( this.pageDescriptorService ) + .contentDataSerializer( contentDataSerializer ) + .formDefaultValuesProcessor( ( form, data ) -> { + } ) + .build(); + + final Content createdContent = command.execute(); + assertNotNull( createdContent ); + assertTrue( createdContent.hasExtraData() ); + } + private CreateContentParams.Builder createContentParams() { PropertyTree existingContentData = new PropertyTree();