diff --git a/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/AttachmentUrlHandler.java b/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/AttachmentUrlHandler.java index 4d45c2938f0..3a33a00df22 100644 --- a/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/AttachmentUrlHandler.java +++ b/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/AttachmentUrlHandler.java @@ -1,27 +1,154 @@ package com.enonic.xp.lib.portal.url; -import java.util.Set; +import java.util.Map; +import java.util.Objects; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import com.enonic.xp.portal.PortalRequest; import com.enonic.xp.portal.url.AttachmentUrlParams; +import com.enonic.xp.portal.url.PortalUrlService; +import com.enonic.xp.script.ScriptValue; +import com.enonic.xp.script.bean.BeanContext; +import com.enonic.xp.script.bean.ScriptBean; public final class AttachmentUrlHandler - extends AbstractUrlHandler + implements ScriptBean { - private static final Set VALID_URL_PROPERTY_KEYS = - Set.of( "id", "path", "name", "type", "download", "label", "params" ); + private PortalRequest request; + + private PortalUrlService urlService; + + private String id; + + private String path; + + private String urlType; + + private String name; + + private String label; + + private String projectName; + + private String branch; + + private String baseUrlKey; + + private boolean offline; + + private boolean download; + + private Multimap queryParams; @Override - protected String buildUrl( final Multimap map ) + public void initialize( final BeanContext context ) { - final AttachmentUrlParams params = new AttachmentUrlParams().portalRequest( this.request ).setAsMap( map ); - return this.urlService.attachmentUrl( params ); + this.request = context.getBinding( PortalRequest.class ).get(); + this.urlService = context.getService( PortalUrlService.class ).get(); } - @Override - protected boolean isValidParam( final String param ) + public void setId( final String id ) { - return VALID_URL_PROPERTY_KEYS.contains( param ); + this.id = id; + } + + public void setPath( final String path ) + { + this.path = path; + } + + public void setUrlType( final String urlType ) + { + this.urlType = urlType; + } + + public void setName( final String name ) + { + this.name = name; + } + + public void setLabel( final String label ) + { + this.label = label; + } + + public void setOffline( final boolean offline ) + { + this.offline = offline; + } + + public void setProjectName( final String projectName ) + { + this.projectName = projectName; + } + + public void setBranch( final String branch ) + { + this.branch = branch; + } + + public void setBaseUrlKey( final String baseUrlKey ) + { + this.baseUrlKey = baseUrlKey; + } + + public void setOffline( final Boolean offline ) + { + this.offline = Objects.requireNonNullElse( offline, false ); + } + + public void setDownload( final Boolean download ) + { + this.download = Objects.requireNonNullElse( download, false ); + } + + public void setQueryParams( final ScriptValue params ) + { + if ( params == null ) + { + return; + } + + this.queryParams = HashMultimap.create(); + + for ( final Map.Entry param : params.getMap().entrySet() ) + { + final Object value = param.getValue(); + if ( value instanceof Iterable values ) + { + for ( final Object v : values ) + { + queryParams.put( param.getKey(), v.toString() ); + } + } + else + { + queryParams.put( param.getKey(), value.toString() ); + } + } + } + + public String createUrl() + { + final AttachmentUrlParams params = new AttachmentUrlParams().portalRequest( this.request ) + .id( this.id ) + .path( this.path ) + .type( this.urlType ) + .name( this.name ) + .label( this.label ) + .download( this.download ) + .projectName( this.projectName ) + .branch( this.branch ) + .baseUrlKey( this.baseUrlKey ) + .offline( this.offline ); + + if ( this.queryParams != null ) + { + this.queryParams.forEach( params::param ); + } + + return urlService.attachmentUrl( params ); } } diff --git a/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/ImageUrlHandler.java b/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/ImageUrlHandler.java index 64796d681a5..b46f6cdea78 100644 --- a/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/ImageUrlHandler.java +++ b/modules/lib/lib-portal/src/main/java/com/enonic/xp/lib/portal/url/ImageUrlHandler.java @@ -1,30 +1,166 @@ package com.enonic.xp.lib.portal.url; -import java.util.Set; +import java.util.Map; +import java.util.Objects; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import com.enonic.xp.portal.PortalRequest; import com.enonic.xp.portal.url.ImageUrlParams; +import com.enonic.xp.portal.url.PortalUrlService; +import com.enonic.xp.script.ScriptValue; +import com.enonic.xp.script.bean.BeanContext; +import com.enonic.xp.script.bean.ScriptBean; public final class ImageUrlHandler - extends AbstractUrlHandler + implements ScriptBean { - private static final Set VALID_URL_PROPERTY_KEYS = Set.of( "id", "path", "scale", "quality", "background", "format", "filter", "contextPath", "type", "params" ); + private PortalRequest request; + + private PortalUrlService urlService; + + private String id; + + private String path; + + private String urlType; + + private String background; + + private Integer quality; + + private String filter; + + private String format; + + private String scale; + + private String projectName; + + private String branch; + + private String baseUrlKey; + + private boolean offline; + + private Multimap queryParams; @Override - protected String buildUrl( final Multimap map ) + public void initialize( final BeanContext context ) + { + this.request = context.getBinding( PortalRequest.class ).get(); + this.urlService = context.getService( PortalUrlService.class ).get(); + } + + public void setId( final String id ) { - final ImageUrlParams params = new ImageUrlParams(). - setAsMap( map ). - portalRequest( request ). - validate(); + this.id = id; + } - return this.urlService.imageUrl( params ); + public void setPath( final String path ) + { + this.path = path; } - @Override - protected boolean isValidParam( final String param ) + public void setUrlType( final String urlType ) + { + this.urlType = urlType; + } + + public void setBackground( final String background ) + { + this.background = background; + } + + public void setQuality( final Integer quality ) + { + this.quality = quality; + } + + public void setFilter( final String filter ) { - return VALID_URL_PROPERTY_KEYS.contains( param ); + this.filter = filter; } + + public void setFormat( final String format ) + { + this.format = format; + } + + public void setScale( final String scale ) + { + this.scale = scale; + } + + public void setProjectName( final String projectName ) + { + this.projectName = projectName; + } + + public void setBranch( final String branch ) + { + this.branch = branch; + } + + public void setBaseUrlKey( final String baseUrlKey ) + { + this.baseUrlKey = baseUrlKey; + } + + public void setOffline( final Boolean offline ) + { + this.offline = Objects.requireNonNullElse( offline, false ); + } + + public void setQueryParams( final ScriptValue params ) + { + if ( params == null ) + { + return; + } + + this.queryParams = HashMultimap.create(); + + for ( final Map.Entry param : params.getMap().entrySet() ) + { + final Object value = param.getValue(); + if ( value instanceof Iterable values ) + { + for ( final Object v : values ) + { + queryParams.put( param.getKey(), v.toString() ); + } + } + else + { + queryParams.put( param.getKey(), value.toString() ); + } + } + } + + public String createUrl() + { + final ImageUrlParams params = new ImageUrlParams().portalRequest( this.request ) + .id( this.id ) + .path( this.path ) + .type( this.urlType ) + .background( this.background ) + .quality( this.quality ) + .filter( this.filter ) + .format( this.format ) + .scale( this.scale ) + .projectName( this.projectName ) + .branch( this.branch ) + .baseUrlKey( this.baseUrlKey ) + .offline( this.offline ); + + if ( this.queryParams != null ) + { + this.queryParams.forEach( params::param ); + } + + return this.urlService.imageUrl( params ); + } + } diff --git a/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts b/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts index fca9d82b3a7..0b6b17b7760 100644 --- a/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts +++ b/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts @@ -13,14 +13,7 @@ declare global { } } -import type { - ByteSource, - Component, - Content, - NestedRecord, - Region, - ScriptValue, -} from '@enonic-types/core'; +import type {ByteSource, Component, Content, Region, ScriptValue,} from '@enonic-types/core'; export type { Attachment, @@ -31,7 +24,7 @@ export type { } from '@enonic-types/core'; function checkRequired(obj: T, name: keyof T): void { - if (obj == null || obj[name] === null) { + if (obj == null || obj[name] == null) { throw `Parameter '${String(name)}' is required`; } } @@ -49,7 +42,7 @@ export interface SiteConfig { export type Without = { [P in Exclude]?: never }; export type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; -export type IdXorPath = XOR<{id: string}, {path: string}>; +export type IdXorPath = XOR<{ id: string }, { path: string }>; export interface AssetUrlParams { path: string; @@ -96,10 +89,40 @@ export type ImageUrlParams = IdXorPath & { | `wide(${number},${number})` | `width(${number})` | 'full'; + project?: string; + branch?: string; + baseUrlKey?: string; + offline?: boolean | undefined; }; interface ImageUrlHandler { - createUrl(value: object): string; + setId(value?: string | null): void; + + setPath(value?: string | null): void; + + setUrlType(value?: string | null): void; + + setQueryParams(value?: ScriptValue | null): void; + + setProjectName(value?: string | null): void; + + setBranch(value?: string | null): void; + + setBackground(value?: string | null): void; + + setQuality(value?: number | null): void; + + setFilter(value?: string | null): void; + + setFormat(value?: string | null): void; + + setScale(value: string): void; + + setOffline(value: boolean): void; + + setBaseUrlKey(value?: string | null): void; + + createUrl(): string; } /** @@ -116,13 +139,34 @@ interface ImageUrlHandler { * @param {string} [params.format] Format of the image. * @param {string} [params.filter] A number of filters are available to alter the image appearance, for example, blur(3), grayscale(), rounded(5), etc. * @param {string} [params.type=server] URL type. Either `server` (server-relative URL) or `absolute`. + * @param {string} [params.projectName] Name of the project. + * @param {string} [params.branch] Name of the branch. + * @param {string} [params.baseUrlKey] Key of the content. + * @param {boolean} [params.offline=false] Set to true if the URL should be generated without context of the current request. * @param {object} [params.params] Custom parameters to append to the url. * * @returns {string} The generated URL. */ export function imageUrl(params: ImageUrlParams): string { const bean: ImageUrlHandler = __.newBean('com.enonic.xp.lib.portal.url.ImageUrlHandler'); - return bean.createUrl(__.toScriptValue(params)); + + checkRequired(params, 'scale'); + + bean.setId(__.nullOrValue(params.id)); + bean.setPath(__.nullOrValue(params.path)); + bean.setUrlType(params.type || 'server'); + bean.setQueryParams(__.toScriptValue(params.params)); + bean.setBackground(__.nullOrValue(params.background)); + bean.setQuality(__.nullOrValue(params.quality)); + bean.setFilter(__.nullOrValue(params.filter)); + bean.setFormat(__.nullOrValue(params.format)); + bean.setScale(params.scale); + bean.setProjectName(__.nullOrValue(params.project)); + bean.setBranch(__.nullOrValue(params.branch)); + bean.setBaseUrlKey(__.nullOrValue(params.baseUrlKey)); + bean.setOffline(params.offline || false); + + return bean.createUrl(); } export interface ComponentUrlParams { @@ -164,10 +208,36 @@ export interface AttachmentUrlParams { download?: boolean; type?: 'server' | 'absolute'; params?: object; + project?: string; + branch?: string; + baseUrlKey?: string; + offline?: boolean; } interface AttachmentUrlHandler { - createUrl(value: object): string; + setId(value?: string | null): void; + + setPath(value?: string | null): void; + + setUrlType(value?: string | null): void; + + setQueryParams(value?: ScriptValue | null): void; + + setName(value?: string | null): void; + + setLabel(value?: string | null): void; + + setProjectName(value?: string | null): void; + + setBranch(value?: string | null): void; + + setBaseUrlKey(value?: string | null): void; + + setOffline(value: boolean): void; + + setDownload(value: boolean): void; + + createUrl(): string; } /** @@ -182,13 +252,30 @@ interface AttachmentUrlHandler { * @param {string} [params.label=source] Label of the attachment. * @param {boolean} [params.download=false] Set to true if the disposition header should be set to attachment. * @param {string} [params.type=server] URL type. Either `server` (server-relative URL) or `absolute`. + * @param {string} [params.projectName] Name of the project. + * @param {string} [params.branch] Name of the branch. + * @param {string} [params.baseUrlKey] Key of the content. + * @param {boolean} [params.offline=false] Set to true if the URL should be generated without context of the current request. * @param {object} [params.params] Custom parameters to append to the url. * * @returns {string} The generated URL. */ export function attachmentUrl(params: AttachmentUrlParams): string { const bean: AttachmentUrlHandler = __.newBean('com.enonic.xp.lib.portal.url.AttachmentUrlHandler'); - return bean.createUrl(__.toScriptValue(params)); + + bean.setId(__.nullOrValue(params.id)); + bean.setPath(__.nullOrValue(params.path)); + bean.setUrlType(params.type || 'server'); + bean.setName(__.nullOrValue(params.name)); + bean.setLabel(__.nullOrValue(params.label)); + bean.setProjectName(__.nullOrValue(params.project)); + bean.setBranch(__.nullOrValue(params.branch)); + bean.setBaseUrlKey(__.nullOrValue(params.baseUrlKey)); + bean.setOffline(params.offline || false); + bean.setDownload(params.download || false); + bean.setQueryParams(__.toScriptValue(params.params)); + + return bean.createUrl(); } export type PageUrlParams = IdXorPath & { @@ -443,7 +530,8 @@ interface GetCurrentSiteConfigHandler { * @returns {object|null} The site configuration for current application as JSON. */ export function getSiteConfig>(): Config | null { - const bean: GetCurrentSiteConfigHandler = __.newBean('com.enonic.xp.lib.portal.current.GetCurrentSiteConfigHandler'); + const bean: GetCurrentSiteConfigHandler = __.newBean( + 'com.enonic.xp.lib.portal.current.GetCurrentSiteConfigHandler'); return __.toNativeObject(bean.execute()); } @@ -460,7 +548,8 @@ interface GetCurrentContentHandler { * @returns {object|null} The current content as JSON. */ export function getContent = Content>(): Hit | null { - const bean: GetCurrentContentHandler = __.newBean('com.enonic.xp.lib.portal.current.GetCurrentContentHandler'); + const bean: GetCurrentContentHandler = __.newBean( + 'com.enonic.xp.lib.portal.current.GetCurrentContentHandler'); return __.toNativeObject(bean.execute()); } @@ -479,7 +568,8 @@ interface GetCurrentComponentHandler<_Component extends Component = Component> { export function getComponent< _Component extends Component = Component >(): _Component | null { - const bean: GetCurrentComponentHandler<_Component> = __.newBean>('com.enonic.xp.lib.portal.current.GetCurrentComponentHandler'); + const bean: GetCurrentComponentHandler<_Component> = __.newBean>( + 'com.enonic.xp.lib.portal.current.GetCurrentComponentHandler'); return __.toNativeObject(bean.execute()); } @@ -495,7 +585,8 @@ interface GetCurrentIdProviderKeyHandler { * @returns {string|null} The current id provider as JSON. */ export function getIdProviderKey(): string | null { - const bean: GetCurrentIdProviderKeyHandler = __.newBean('com.enonic.xp.lib.portal.current.GetCurrentIdProviderKeyHandler'); + const bean: GetCurrentIdProviderKeyHandler = __.newBean( + 'com.enonic.xp.lib.portal.current.GetCurrentIdProviderKeyHandler'); return __.toNativeObject(bean.execute()); } diff --git a/modules/lib/lib-portal/src/test/resources/test/url-test.js b/modules/lib/lib-portal/src/test/resources/test/url-test.js index ff7697e18ff..791ff5df92b 100644 --- a/modules/lib/lib-portal/src/test/resources/test/url-test.js +++ b/modules/lib/lib-portal/src/test/resources/test/url-test.js @@ -103,6 +103,7 @@ exports.imageUrlTest = function () { id: '123', background: 'ffffff', quality: 90, + scale: 'block(200,100)', filter: 'scale(1,1)', params: { a: 1, @@ -111,7 +112,7 @@ exports.imageUrlTest = function () { }); // NOTE: This is not the actual url. Only a mock representation. - assert.assertEquals('ImageUrlParams{type=server, params={a=[1], b=[1, 2]}, id=123, quality=90, filter=scale(1,1), background=ffffff}', + assert.assertEquals('ImageUrlParams{type=server, params={a=[1], b=[1, 2]}, id=123, quality=90, filter=scale(1,1), background=ffffff, scale=block(200,100)}', result); return true; }; @@ -121,6 +122,7 @@ exports.imageUrlTest_unknownProperty = function () { id: '123', background: 'ffffff', quality: 90, + scale: 'block(200,100)', filter: 'scale(1,1)', params: { a: 1, @@ -129,7 +131,7 @@ exports.imageUrlTest_unknownProperty = function () { unknownProperty: 'value' }); - assert.assertEquals('ImageUrlParams{type=server, params={a=[1], b=[1, 2]}, id=123, quality=90, filter=scale(1,1), background=ffffff}', + assert.assertEquals('ImageUrlParams{type=server, params={a=[1], b=[1, 2]}, id=123, quality=90, filter=scale(1,1), background=ffffff, scale=block(200,100)}', result); return true; }; diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlGeneratorParams.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlGeneratorParams.java new file mode 100644 index 00000000000..a6d0a3a2efa --- /dev/null +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlGeneratorParams.java @@ -0,0 +1,168 @@ +package com.enonic.xp.portal.url; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import com.enonic.xp.annotation.PublicApi; +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +@PublicApi +public final class AttachmentUrlGeneratorParams +{ + private final BaseUrlStrategy baseUrlStrategy; + + private final Media media; + + private final ProjectName projectName; + + private final Branch branch; + + private final boolean download; + + private final String name; + + private final String label; + + private final Multimap queryParams; + + private AttachmentUrlGeneratorParams( final Builder builder ) + { + this.baseUrlStrategy = Objects.requireNonNull( builder.baseUrlStrategy ); + this.media = Objects.requireNonNull( builder.media ); + this.projectName = builder.projectName; + this.branch = builder.branch; + this.download = builder.download; + this.name = builder.name; + this.label = builder.label; + this.queryParams = builder.queryParams; + } + + public BaseUrlStrategy getBaseUrlStrategy() + { + return baseUrlStrategy; + } + + public Media getMedia() + { + return media; + } + + public ProjectName getProjectName() + { + return projectName; + } + + public Branch getBranch() + { + return branch; + } + + public boolean isDownload() + { + return download; + } + + public String getName() + { + return name; + } + + public String getLabel() + { + return label; + } + + public Multimap getQueryParams() + { + return queryParams; + } + + public static Builder create() + { + return new Builder(); + } + + public static class Builder + { + private BaseUrlStrategy baseUrlStrategy; + + private Media media; + + private ProjectName projectName; + + private Branch branch; + + private boolean download; + + private String name; + + private String label; + + private final Multimap queryParams = HashMultimap.create(); + + public Builder setBaseUrlStrategy( final BaseUrlStrategy baseUrlStrategy ) + { + this.baseUrlStrategy = baseUrlStrategy; + return this; + } + + public Builder setMedia( final Media media ) + { + this.media = media; + return this; + } + + public Builder setProjectName( final ProjectName projectName ) + { + this.projectName = projectName; + return this; + } + + public Builder setBranch( final Branch branch ) + { + this.branch = branch; + return this; + } + + public Builder setDownload( final boolean download ) + { + this.download = download; + return this; + } + + public Builder setName( final String name ) + { + this.name = name; + return this; + } + + public Builder setLabel( final String label ) + { + this.label = label; + return this; + } + + public Builder addQueryParams( final Map> queryParams ) + { + queryParams.forEach( this.queryParams::putAll ); + return this; + } + + public Builder addQueryParam( final String key, final String value ) + { + this.queryParams.put( key, value ); + return this; + } + + public AttachmentUrlGeneratorParams build() + { + return new AttachmentUrlGeneratorParams( this ); + } + } +} diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlParams.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlParams.java index f5ce32e2bf3..27a048d68b3 100644 --- a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlParams.java +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/AttachmentUrlParams.java @@ -1,5 +1,7 @@ package com.enonic.xp.portal.url; +import java.util.Objects; + import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.Multimap; @@ -20,7 +22,15 @@ public final class AttachmentUrlParams private String label; - private boolean download = false; + private boolean download; + + private String projectName; + + private String branch; + + private String baseUrlKey; + + private boolean offline; public String getId() { @@ -47,6 +57,26 @@ public boolean isDownload() return this.download; } + public String getProjectName() + { + return projectName; + } + + public String getBranch() + { + return branch; + } + + public String getBaseUrlKey() + { + return baseUrlKey; + } + + public boolean isOffline() + { + return offline; + } + public AttachmentUrlParams id( final String value ) { this.id = Strings.emptyToNull( value ); @@ -82,6 +112,30 @@ public AttachmentUrlParams download( final boolean value ) return this; } + public AttachmentUrlParams projectName( final String projectName ) + { + this.projectName = projectName; + return this; + } + + public AttachmentUrlParams branch( final String branch ) + { + this.branch = branch; + return this; + } + + public AttachmentUrlParams baseUrlKey( final String baseUrlKey ) + { + this.baseUrlKey = baseUrlKey; + return this; + } + + public AttachmentUrlParams offline( final Boolean offline ) + { + this.offline = Objects.requireNonNullElse( offline, false ); + return this; + } + @Override public AttachmentUrlParams setAsMap( final Multimap map ) { diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/BaseUrlStrategy.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/BaseUrlStrategy.java new file mode 100644 index 00000000000..269c35afb4e --- /dev/null +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/BaseUrlStrategy.java @@ -0,0 +1,9 @@ +package com.enonic.xp.portal.url; + +import com.enonic.xp.annotation.PublicApi; + +@PublicApi +public interface BaseUrlStrategy +{ + String generateBaseUrl(); +} diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlGeneratorParams.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlGeneratorParams.java new file mode 100644 index 00000000000..d5935a4b43b --- /dev/null +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlGeneratorParams.java @@ -0,0 +1,200 @@ +package com.enonic.xp.portal.url; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import com.enonic.xp.annotation.PublicApi; +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +@PublicApi +public final class ImageUrlGeneratorParams +{ + private final BaseUrlStrategy baseUrlStrategy; + + private final Media media; + + private final ProjectName projectName; + + private final Branch branch; + + private final String background; + + private final Integer quality; + + private final String filter; + + private final String format; + + private final String scale; + + private final Multimap queryParams; + + private ImageUrlGeneratorParams( final Builder builder ) + { + this.baseUrlStrategy = Objects.requireNonNull( builder.baseUrlStrategy ); + this.media = Objects.requireNonNull( builder.media ); + this.projectName = Objects.requireNonNull( builder.projectName ); + this.branch = Objects.requireNonNull( builder.branch ); + this.scale = Objects.requireNonNull( builder.scale ); + this.background = builder.background; + this.quality = builder.quality; + this.filter = builder.filter; + this.format = builder.format; + this.queryParams = builder.queryParams; + } + + public BaseUrlStrategy getBaseUrlStrategy() + { + return baseUrlStrategy; + } + + public Media getMedia() + { + return media; + } + + public ProjectName getProjectName() + { + return projectName; + } + + public Branch getBranch() + { + return branch; + } + + public String getBackground() + { + return background; + } + + public Integer getQuality() + { + return quality; + } + + public String getFilter() + { + return filter; + } + + public String getFormat() + { + return format; + } + + public String getScale() + { + return scale; + } + + public Multimap getQueryParams() + { + return queryParams; + } + + public static Builder create() + { + return new Builder(); + } + + public static class Builder + { + private BaseUrlStrategy baseUrlStrategy; + + private Media media; + + private ProjectName projectName; + + private Branch branch; + + private String background; + + private Integer quality; + + private String filter; + + private String format; + + private String scale; + + private final Multimap queryParams = HashMultimap.create(); + + public Builder setBaseUrlStrategy( final BaseUrlStrategy baseUrlStrategy ) + { + this.baseUrlStrategy = baseUrlStrategy; + return this; + } + + public Builder setMedia( final Media media ) + { + this.media = media; + return this; + } + + public Builder setProjectName( final ProjectName projectName ) + { + this.projectName = projectName; + return this; + } + + public Builder setBranch( final Branch branch ) + { + this.branch = branch; + return this; + } + + public Builder setBackground( final String background ) + { + this.background = background; + return this; + } + + public Builder setQuality( final Integer quality ) + { + this.quality = quality; + return this; + } + + public Builder setFilter( final String filter ) + { + this.filter = filter; + return this; + } + + public Builder setFormat( final String format ) + { + this.format = format; + return this; + } + + public Builder setScale( final String scale ) + { + this.scale = scale; + return this; + } + + public Builder addQueryParams( final Map> queryParams ) + { + queryParams.forEach( this.queryParams::putAll ); + return this; + } + + public Builder addQueryParam( final String key, final String value ) + { + this.queryParams.put( key, value ); + return this; + } + + public ImageUrlGeneratorParams build() + { + return new ImageUrlGeneratorParams( this ); + } + } +} diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlParams.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlParams.java index c52921bdf2b..76b283b5f77 100644 --- a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlParams.java +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/ImageUrlParams.java @@ -1,5 +1,7 @@ package com.enonic.xp.portal.url; +import java.util.Objects; + import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -27,6 +29,14 @@ public final class ImageUrlParams private String scale; + private String projectName; + + private String branch; + + private String baseUrlKey; + + private boolean offline; + public String getId() { return this.id; @@ -62,6 +72,26 @@ public String getScale() return this.scale; } + public String getProjectName() + { + return projectName; + } + + public String getBranch() + { + return branch; + } + + public String getBaseUrlKey() + { + return baseUrlKey; + } + + public boolean isOffline() + { + return offline; + } + public ImageUrlParams id( final String value ) { this.id = Strings.emptyToNull( value ); @@ -109,6 +139,30 @@ public ImageUrlParams scale( final String value ) return this; } + public ImageUrlParams projectName( final String projectName ) + { + this.projectName = projectName; + return this; + } + + public ImageUrlParams branch( final String branch ) + { + this.branch = branch; + return this; + } + + public ImageUrlParams baseUrlKey( final String baseUrlKey ) + { + this.baseUrlKey = baseUrlKey; + return this; + } + + public ImageUrlParams offline( final Boolean offline ) + { + this.offline = Objects.requireNonNullElse( offline, false ); + return this; + } + @Override public ImageUrlParams setAsMap( final Multimap map ) { @@ -139,9 +193,8 @@ protected void buildToString( final MoreObjects.ToStringHelper helper ) public ImageUrlParams validate() { - Preconditions.checkState( - getPortalRequest().getContent() != null || id != null || path != null, - "id, path or content must be set" ); + Preconditions.checkState( getPortalRequest().getContent() != null || id != null || path != null, + "id, path or content must be set" ); return this; } } diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/PortalUrlService.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/PortalUrlService.java index 1bd2ea74b01..b8cf7c6bb9e 100644 --- a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/PortalUrlService.java +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/PortalUrlService.java @@ -24,4 +24,8 @@ public interface PortalUrlService String processHtml( ProcessHtmlParams params ); String apiUrl( ApiUrlParams params ); + + String imageUrl( ImageUrlGeneratorParams params ); + + String attachmentUrl( AttachmentUrlGeneratorParams params ); } diff --git a/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/UrlStrategyFacade.java b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/UrlStrategyFacade.java new file mode 100644 index 00000000000..c908fa37bce --- /dev/null +++ b/modules/portal/portal-api/src/main/java/com/enonic/xp/portal/url/UrlStrategyFacade.java @@ -0,0 +1,15 @@ +package com.enonic.xp.portal.url; + +import com.enonic.xp.annotation.PublicApi; + +@PublicApi +public interface UrlStrategyFacade +{ + ImageUrlGeneratorParams offlineImageUrlParams( ImageUrlParams params ); + + ImageUrlGeneratorParams requestImageUrlParams( ImageUrlParams params ); + + AttachmentUrlGeneratorParams offlineAttachmentUrlParams( AttachmentUrlParams params ); + + AttachmentUrlGeneratorParams requestAttachmentUrlParams( AttachmentUrlParams params ); +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/idprovider/PortalRequestAdapter.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/idprovider/PortalRequestAdapter.java index efe0268c9f0..7f34d3455bb 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/idprovider/PortalRequestAdapter.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/idprovider/PortalRequestAdapter.java @@ -175,6 +175,10 @@ else if ( requestURI.startsWith( WEBAPP_PREFIX ) ) result.setBaseUri( WEBAPP_PREFIX + matcher.group( 0 ) ); } } + else if ( requestURI.startsWith( "/api/" ) ) + { + result.setBaseUri( "/api" ); + } } private static String subPath( String requestURI, String prefix ) diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategy.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategy.java new file mode 100644 index 00000000000..b31a31aba74 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategy.java @@ -0,0 +1,95 @@ +package com.enonic.xp.portal.impl.url; + +import java.util.Objects; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; + +import com.enonic.xp.attachment.Attachment; +import com.enonic.xp.attachment.Attachments; +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.ContentConstants; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.appendParams; +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.appendPart; + +final class AttachmentMediaPathStrategy + implements PathStrategy +{ + private final AttachmentMediaPathStrategyParams params; + + AttachmentMediaPathStrategy( final AttachmentMediaPathStrategyParams params ) + { + this.params = params; + } + + @Override + public String generatePath() + { + final Media media = params.getMedia(); + final ProjectName project = params.getProjectName(); + final Branch branch = params.getBranch(); + + final StringBuilder url = new StringBuilder(); + + appendPart( url, "media" ); + appendPart( url, "attachment" ); + appendPart( url, project + ( ContentConstants.BRANCH_MASTER.equals( branch ) ? "" : ":" + branch ) ); + + final Attachment attachment = resolveAttachment(); + final String hash = resolveHash( attachment ); + + appendPart( url, media.getId().toString() + ( hash != null ? ":" + hash : "" ) ); + appendPart( url, attachment.getName() ); + + final Multimap queryParams = resolveQueryParams(); + appendParams( url, queryParams.entries() ); + + return url.toString(); + } + + private Multimap resolveQueryParams() + { + final Multimap queryParams = LinkedListMultimap.create(); + if ( this.params.isDownload() ) + { + queryParams.put( "download", null ); + } + if ( params.getQueryParams() != null ) + { + queryParams.putAll( params.getQueryParams() ); + } + + return queryParams; + } + + private Attachment resolveAttachment() + { + final Attachments attachments = params.getMedia().getAttachments(); + + final String attachmentNameOrLabel = + Objects.requireNonNullElseGet( params.getName(), () -> Objects.requireNonNullElse( params.getLabel(), "source" ) ); + + Attachment attachment = attachments.byName( attachmentNameOrLabel ); + if ( attachment == null ) + { + attachment = attachments.byLabel( attachmentNameOrLabel ); + } + + if ( attachment != null ) + { + return attachment; + } + + throw new IllegalArgumentException( + String.format( "Could not find attachment with name/label [%s] on content [%s]", attachmentNameOrLabel, + params.getMedia().getId() ) ); + } + + private String resolveHash( final Attachment attachment ) + { + return attachment.getSha512() != null ? attachment.getSha512().substring( 0, 32 ) : null; + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategyParams.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategyParams.java new file mode 100644 index 00000000000..60c310c55a3 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentMediaPathStrategyParams.java @@ -0,0 +1,139 @@ +package com.enonic.xp.portal.impl.url; + +import com.google.common.collect.Multimap; + +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +final class AttachmentMediaPathStrategyParams +{ + private final Media media; + + private final ProjectName projectName; + + private final Branch branch; + + private final boolean download; + + private final String name; + + private final String label; + + private final Multimap queryParams; + + private AttachmentMediaPathStrategyParams( final Builder builder ) + { + this.media = builder.media; + this.projectName = builder.projectName; + this.branch = builder.branch; + this.download = builder.download; + this.name = builder.name; + this.label = builder.label; + this.queryParams = builder.queryParams; + } + + public Media getMedia() + { + return media; + } + + public ProjectName getProjectName() + { + return projectName; + } + + public Branch getBranch() + { + return branch; + } + + public boolean isDownload() + { + return download; + } + + public String getName() + { + return name; + } + + public String getLabel() + { + return label; + } + + public Multimap getQueryParams() + { + return queryParams; + } + + public static Builder create() + { + return new Builder(); + } + + static class Builder + { + private Media media; + + private ProjectName projectName; + + private Branch branch; + + private boolean download; + + private String name; + + private String label; + + private Multimap queryParams; + + public Builder setMedia( final Media media ) + { + this.media = media; + return this; + } + + public Builder setProjectName( final ProjectName projectName ) + { + this.projectName = projectName; + return this; + } + + public Builder setBranch( final Branch branch ) + { + this.branch = branch; + return this; + } + + public Builder setDownload( final boolean download ) + { + this.download = download; + return this; + } + + public Builder setName( final String name ) + { + this.name = name; + return this; + } + + public Builder setLabel( final String label ) + { + this.label = label; + return this; + } + + public Builder setQueryParams( final Multimap queryParams ) + { + this.queryParams = queryParams; + return this; + } + + public AttachmentMediaPathStrategyParams build() + { + return new AttachmentMediaPathStrategyParams( this ); + } + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentUrlBuilder.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentUrlBuilder.java index 09860a80197..4d99ace58f1 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentUrlBuilder.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/AttachmentUrlBuilder.java @@ -4,100 +4,30 @@ import com.enonic.xp.attachment.Attachment; import com.enonic.xp.attachment.Attachments; -import com.enonic.xp.branch.Branch; import com.enonic.xp.content.Content; -import com.enonic.xp.content.ContentConstants; -import com.enonic.xp.portal.impl.ContentResolverResult; -import com.enonic.xp.portal.impl.VirtualHostContextHelper; import com.enonic.xp.portal.url.AttachmentUrlParams; -import com.enonic.xp.repository.RepositoryUtils; final class AttachmentUrlBuilder extends PortalUrlBuilder { - private boolean legacyAttachmentServiceEnabled; - @Override protected void buildUrl( final StringBuilder url, final Multimap params ) { - final boolean isSlashAPI = portalRequest.getRawPath().startsWith( "/api/" ); - final String projectName = RepositoryUtils.getContentRepoName( this.portalRequest.getRepositoryId() ); - final Branch branch = this.portalRequest.getBranch(); - - if ( isSlashAPI ) - { - url.setLength( 0 ); - appendPart( url, "attachment" ); - appendPart( url, branch == ContentConstants.BRANCH_DRAFT ? projectName + ":" + branch.getValue() : projectName ); - if ( this.params.isDownload() ) - { - params.put( "download", null ); - } - } - else - { - super.buildUrl( url, params ); + super.buildUrl( url, params ); - if ( legacyAttachmentServiceEnabled ) - { - appendPart( url, this.portalRequest.getContentPath().toString() ); - appendPart( url, "_" ); - appendPart( url, "attachment" ); - appendPart( url, this.params.isDownload() ? "download" : "inline" ); - } - else - { - final ContentResolverResult contentResolverResult = - new com.enonic.xp.portal.impl.ContentResolver( contentService ).resolve( portalRequest ); - - if ( contentResolverResult.getNearestSite() != null ) - { - appendPart( url, contentResolverResult.getNearestSite().getPath().toString() ); - } - else - { - url.setLength( 0 ); - appendPart( url, "site" ); - appendPart( url, projectName ); - appendPart( url, branch.getValue() ); - } - - appendPart( url, "_" ); - appendPart( url, "media" ); - appendPart( url, "attachment" ); - appendPart( url, branch == ContentConstants.BRANCH_DRAFT ? projectName + ":" + branch.getValue() : projectName ); - if ( this.params.isDownload() ) - { - params.put( "download", null ); - } - } - } + appendPart( url, this.portalRequest.getContentPath().toString() ); + appendPart( url, "_" ); + appendPart( url, "attachment" ); + appendPart( url, this.params.isDownload() ? "download" : "inline" ); final Content content = resolveContent(); final Attachment attachment = resolveAttachment( content ); final String hash = resolveHash( content, attachment ); - appendPart( url, content.getId().toString() + ( hash != null ? ":" + hash : "" ) ); + appendPart( url, content.getId().toString() + ":" + hash ); appendPart( url, attachment.getName() ); } - @Override - protected String getBaseUrl() - { - return VirtualHostContextHelper.getMediaServiceBaseUrl(); - } - - @Override - protected String getTargetUriPrefix() - { - return "/api/media"; - } - - public void setLegacyAttachmentServiceEnabled( final boolean legacyAttachmentServiceEnabled ) - { - this.legacyAttachmentServiceEnabled = legacyAttachmentServiceEnabled; - } - private Content resolveContent() { final ContentResolver contentResolver = new ContentResolver().portalRequest( this.portalRequest ) @@ -138,13 +68,6 @@ private Attachment resolveAttachment( final Content content ) private String resolveHash( final Content content, final Attachment attachment ) { - if ( legacyAttachmentServiceEnabled ) - { - return this.contentService.getBinaryKey( content.getId(), attachment.getBinaryReference() ); - } - else - { - return attachment.getSha512() != null ? attachment.getSha512().substring( 0, 32 ) : null; - } + return this.contentService.getBinaryKey( content.getId(), attachment.getBinaryReference() ); } } diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategy.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategy.java new file mode 100644 index 00000000000..813329eb7b6 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategy.java @@ -0,0 +1,124 @@ +package com.enonic.xp.portal.impl.url; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; + +import com.enonic.xp.attachment.Attachment; +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Content; +import com.enonic.xp.content.ContentConstants; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.appendParams; +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.appendPart; +import static com.google.common.base.Strings.isNullOrEmpty; + +final class ImageMediaPathStrategy + implements PathStrategy +{ + private final ImageMediaPathStrategyParams params; + + ImageMediaPathStrategy( final ImageMediaPathStrategyParams params ) + { + this.params = Objects.requireNonNull( params ); + } + + @Override + public String generatePath() + { + final Media media = params.getMedia(); + final ProjectName project = params.getProjectName(); + final Branch branch = params.getBranch(); + + final String hash = resolveHash( media ); + final String name = resolveName( media, params.getFormat() ); + final String scale = resolveScale( params.getScale() ); + + final StringBuilder url = new StringBuilder(); + + appendPart( url, "media" ); + appendPart( url, "image" ); + appendPart( url, project + ( ContentConstants.BRANCH_MASTER.equals( branch ) ? "" : ":" + branch ) ); + appendPart( url, media.getId() + ( hash != null ? ":" + hash : "" ) ); + appendPart( url, scale ); + appendPart( url, name ); + + final Multimap queryParams = resolveQueryParams(); + appendParams( url, queryParams.entries() ); + + return url.toString(); + } + + private Multimap resolveQueryParams() + { + final Multimap queryParams = LinkedListMultimap.create(); + if ( this.params.getQuality() != null ) + { + queryParams.put( "quality", this.params.getQuality().toString() ); + } + if ( this.params.getBackground() != null ) + { + queryParams.put( "background", this.params.getBackground() ); + } + if ( this.params.getFilter() != null ) + { + queryParams.put( "filter", this.params.getFilter() ); + } + if ( params.getQueryParams() != null ) + { + queryParams.putAll( params.getQueryParams() ); + } + + return queryParams; + } + + private String resolveHash( final Media media ) + { + final Attachment attachment = media.getMediaAttachment(); + + if ( attachment.getSha512() == null ) + { + return null; + } + + return Hashing.sha1() + .newHasher() + .putString( attachment.getSha512().substring( 0, 32 ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getFocalPoint() ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getCropping() ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getOrientation() ), StandardCharsets.UTF_8 ) + .hash() + .toString(); + } + + private String resolveName( final Content media, final String format ) + { + final String name = media.getName().toString(); + + if ( format != null ) + { + final String extension = Files.getFileExtension( name ); + if ( isNullOrEmpty( extension ) || !format.equals( extension ) ) + { + return name + "." + format; + } + } + return name; + } + + private String resolveScale( final String scale ) + { + if ( scale == null ) + { + throw new IllegalArgumentException( "Missing mandatory parameter 'scale' for image URL" ); + } + + return scale.replaceAll( "\\s", "" ).replaceAll( "[(,]", "-" ).replace( ")", "" ); + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategyParams.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategyParams.java new file mode 100644 index 00000000000..830aa9dfc13 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageMediaPathStrategyParams.java @@ -0,0 +1,173 @@ +package com.enonic.xp.portal.impl.url; + +import java.util.Objects; + +import com.google.common.collect.Multimap; + +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Media; +import com.enonic.xp.project.ProjectName; + +final class ImageMediaPathStrategyParams +{ + private final Media media; + + private final ProjectName projectName; + + private final Branch branch; + + private final String scale; + + private final String format; + + private final String background; + + private final Integer quality; + + private final String filter; + + private final Multimap queryParams; + + private ImageMediaPathStrategyParams( final Builder builder ) + { + this.media = Objects.requireNonNull( builder.media ); + this.projectName = Objects.requireNonNull( builder.projectName ); + this.branch = Objects.requireNonNull( builder.branch ); + this.scale = Objects.requireNonNull( builder.scale ); + this.format = builder.format; + this.background = builder.background; + this.quality = builder.quality; + this.filter = builder.filter; + this.queryParams = builder.queryParams; + } + + public Media getMedia() + { + return media; + } + + public ProjectName getProjectName() + { + return projectName; + } + + public Branch getBranch() + { + return branch; + } + + public String getScale() + { + return scale; + } + + public String getFormat() + { + return format; + } + + public String getBackground() + { + return background; + } + + public Integer getQuality() + { + return quality; + } + + public String getFilter() + { + return filter; + } + + public Multimap getQueryParams() + { + return queryParams; + } + + public static Builder create() + { + return new Builder(); + } + + static class Builder + { + private Media media; + + private ProjectName projectName; + + private Branch branch; + + private String scale; + + private String format; + + private String background; + + private Integer quality; + + private String filter; + + private Multimap queryParams; + + public Builder setMedia( final Media media ) + { + this.media = media; + return this; + } + + public Builder setProjectName( final ProjectName projectName ) + { + this.projectName = projectName; + return this; + } + + public Builder setBranch( final Branch branch ) + { + this.branch = branch; + return this; + } + + public Builder setScale( final String scale ) + { + this.scale = scale; + return this; + } + + public Builder setFormat( final String format ) + { + this.format = format; + return this; + } + + public Builder setBackground( final String background ) + { + this.background = background; + return this; + } + + public Builder setQuality( final Integer quality ) + { + this.quality = quality; + return this; + } + + public Builder setFilter( final String filter ) + { + this.filter = filter; + return this; + } + + public Builder setQueryParams( final Multimap queryParams ) + { + this.queryParams = queryParams; + return this; + } + + public ImageMediaPathStrategyParams build() + { + return new ImageMediaPathStrategyParams( this ); + } + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageUrlBuilder.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageUrlBuilder.java index 31bc331df8b..0db9b3a42f8 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageUrlBuilder.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/ImageUrlBuilder.java @@ -6,25 +6,17 @@ import com.google.common.hash.Hashing; import com.google.common.io.Files; -import com.enonic.xp.attachment.Attachment; -import com.enonic.xp.branch.Branch; import com.enonic.xp.content.Content; -import com.enonic.xp.content.ContentConstants; import com.enonic.xp.content.Media; import com.enonic.xp.context.ContextAccessor; import com.enonic.xp.exception.NotFoundException; -import com.enonic.xp.portal.impl.ContentResolverResult; -import com.enonic.xp.portal.impl.VirtualHostContextHelper; import com.enonic.xp.portal.url.ImageUrlParams; -import com.enonic.xp.repository.RepositoryUtils; import static com.google.common.base.Strings.isNullOrEmpty; final class ImageUrlBuilder extends GenericEndpointUrlBuilder { - private boolean legacyImageServiceEnabled; - ImageUrlBuilder() { super( "image" ); @@ -33,58 +25,14 @@ final class ImageUrlBuilder @Override protected void buildUrl( final StringBuilder url, final Multimap params ) { - final boolean isSlashAPI = portalRequest.getRawPath().startsWith( "/api/" ); - final String projectName = RepositoryUtils.getContentRepoName( this.portalRequest.getRepositoryId() ); - final Branch branch = this.portalRequest.getBranch(); - - if ( isSlashAPI ) - { - params.putAll( this.params.getParams() ); - - url.setLength( 0 ); - appendPart( url, "image" ); - appendPart( url, branch == ContentConstants.BRANCH_DRAFT ? projectName + ":" + branch.getValue() : projectName ); - } - else - { - if ( legacyImageServiceEnabled ) - { - super.buildUrl( url, params ); - } - else - { - params.putAll( this.params.getParams() ); - - final ContentResolverResult contentResolverResult = - new com.enonic.xp.portal.impl.ContentResolver( contentService ).resolve( portalRequest ); - - if ( contentResolverResult.getNearestSite() != null ) - { - appendPart( url, projectName ); - appendPart( url, branch.getValue() ); - appendPart( url, contentResolverResult.getNearestSite().getPath().toString() ); - } - else - { - url.setLength( 0 ); - appendPart( url, "site" ); - appendPart( url, projectName ); - appendPart( url, branch.getValue() ); - } - - appendPart( url, "_" ); - appendPart( url, "media" ); - appendPart( url, "image" ); - appendPart( url, branch == ContentConstants.BRANCH_DRAFT ? projectName + ":" + branch.getValue() : projectName ); - } - } + super.buildUrl( url, params ); final Media media = resolveMedia(); final String hash = resolveHash( media ); final String name = resolveName( media ); final String scale = resolveScale(); - appendPart( url, media.getId() + ( hash != null ? ":" + hash : "" ) ); + appendPart( url, media.getId() + ":" + hash ); appendPart( url, scale ); appendPart( url, name ); @@ -93,23 +41,6 @@ protected void buildUrl( final StringBuilder url, final Multimap addParamIfNeeded( params, "filter", this.params.getFilter() ); } - @Override - protected String getBaseUrl() - { - return VirtualHostContextHelper.getMediaServiceBaseUrl(); - } - - @Override - protected String getTargetUriPrefix() - { - return "/api/media"; - } - - public void setLegacyImageServiceEnabled( final boolean legacyImageServiceEnabled ) - { - this.legacyImageServiceEnabled = legacyImageServiceEnabled; - } - private void addParamIfNeeded( final Multimap params, final String name, final Object value ) { if ( value != null ) @@ -137,23 +68,15 @@ private Media resolveMedia() private String resolveHash( final Media media ) { - if ( legacyImageServiceEnabled ) - { - String binaryKey = this.contentService.getBinaryKey( media.getId(), media.getMediaAttachment().getBinaryReference() ); - return Hashing.sha1() - .newHasher() - .putString( String.valueOf( binaryKey ), StandardCharsets.UTF_8 ) - .putString( String.valueOf( media.getFocalPoint() ), StandardCharsets.UTF_8 ) - .putString( String.valueOf( media.getCropping() ), StandardCharsets.UTF_8 ) - .putString( String.valueOf( media.getOrientation() ), StandardCharsets.UTF_8 ) - .hash() - .toString(); - } - else - { - final Attachment attachment = media.getMediaAttachment(); - return attachment.getSha512() != null ? attachment.getSha512().substring( 0, 32 ) : null; - } + String binaryKey = this.contentService.getBinaryKey( media.getId(), media.getMediaAttachment().getBinaryReference() ); + return Hashing.sha1() + .newHasher() + .putString( String.valueOf( binaryKey ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getFocalPoint() ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getCropping() ), StandardCharsets.UTF_8 ) + .putString( String.valueOf( media.getOrientation() ), StandardCharsets.UTF_8 ) + .hash() + .toString(); } private String resolveName( final Content media ) diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/OfflineBaseUrlStrategy.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/OfflineBaseUrlStrategy.java new file mode 100644 index 00000000000..23e5a126ff1 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/OfflineBaseUrlStrategy.java @@ -0,0 +1,171 @@ +package com.enonic.xp.portal.impl.url; + +import java.net.URI; +import java.util.Objects; + +import com.enonic.xp.app.ApplicationKey; +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Content; +import com.enonic.xp.content.ContentId; +import com.enonic.xp.content.ContentService; +import com.enonic.xp.context.ContextAccessor; +import com.enonic.xp.context.ContextBuilder; +import com.enonic.xp.portal.url.BaseUrlStrategy; +import com.enonic.xp.portal.url.UrlTypeConstants; +import com.enonic.xp.project.Project; +import com.enonic.xp.project.ProjectName; +import com.enonic.xp.project.ProjectService; +import com.enonic.xp.site.Site; +import com.enonic.xp.site.SiteConfig; +import com.enonic.xp.site.SiteConfigs; + +final class OfflineBaseUrlStrategy + implements BaseUrlStrategy +{ + private final ProjectName projectName; + + private final Branch branch; + + private final Content content; + + private final String urlType; + + private final ContentService contentService; + + private final ProjectService projectService; + + OfflineBaseUrlStrategy( final Builder builder ) + { + this.contentService = Objects.requireNonNull( builder.contentService ); + this.projectService = Objects.requireNonNull( builder.projectService ); + this.projectName = Objects.requireNonNull( builder.projectName ); + this.branch = Objects.requireNonNull( builder.branch ); + this.urlType = Objects.requireNonNullElse( builder.urlType, UrlTypeConstants.SERVER_RELATIVE ); + this.content = builder.content; + } + + @Override + public String generateBaseUrl() + { + if ( content == null ) + { + return "/api"; + } + + Site site; + if ( !( content instanceof Site ) ) + { + site = ContextBuilder.copyOf( ContextAccessor.current() ) + .repositoryId( projectName.getRepoId() ) + .branch( branch ) + .build() + .callWith( () -> contentService.getNearestSite( ContentId.from( content.getId() ) ) ); + } + else + { + site = (Site) content; + } + + if ( site != null ) + { + String siteBaseUrl = resolveBaseUrl( site.getSiteConfigs() ); + if ( siteBaseUrl != null ) + { + return normalizeBaseUrl( siteBaseUrl ); + } + } + + Project project = projectService.get( projectName ); + if ( project != null ) + { + String projectBaseUrl = resolveBaseUrl( project.getSiteConfigs() ); + if ( projectBaseUrl != null ) + { + return normalizeBaseUrl( projectBaseUrl ); + } + } + + return "/api"; + } + + public static Builder create() + { + return new Builder(); + } + + private String resolveBaseUrl( final SiteConfigs siteConfigs ) + { + final SiteConfig siteConfig = siteConfigs.get( ApplicationKey.from( "com.enonic.xp.site" ) ); + if ( siteConfig != null ) + { + return siteConfig.getConfig().getString( "baseUrl" ); + } + return null; + } + + private String normalizeBaseUrl( final String baseUrl ) + { + final String path = UrlTypeConstants.SERVER_RELATIVE.equals( urlType ) ? URI.create( baseUrl ).getPath() : baseUrl; + if ( path.endsWith( "/" ) ) + { + return path.substring( 0, path.length() - 1 ) + "/_/"; + } + return path + "/_/"; + } + + static class Builder + { + private ContentService contentService; + + private ProjectService projectService; + + private ProjectName projectName; + + private Branch branch; + + private Content content; + + private String urlType; + + public Builder contentService( final ContentService contentService ) + { + this.contentService = contentService; + return this; + } + + public Builder projectService( final ProjectService projectService ) + { + this.projectService = projectService; + return this; + } + + public Builder projectName( final ProjectName projectName ) + { + this.projectName = projectName; + return this; + } + + public Builder branch( final Branch branch ) + { + this.branch = branch; + return this; + } + + public Builder content( final Content content ) + { + this.content = content; + return this; + } + + public Builder urlType( final String urlType ) + { + this.urlType = urlType; + return this; + } + + public OfflineBaseUrlStrategy build() + { + return new OfflineBaseUrlStrategy( this ); + } + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PageUrlBuilder.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PageUrlBuilder.java index a0cbf6c1377..4d1ca82d09f 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PageUrlBuilder.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PageUrlBuilder.java @@ -4,7 +4,6 @@ import com.enonic.xp.content.ContentPath; import com.enonic.xp.portal.url.PageUrlParams; -import com.enonic.xp.repository.RepositoryUtils; final class PageUrlBuilder extends PortalUrlBuilder @@ -14,13 +13,13 @@ protected void buildUrl( final StringBuilder url, final Multimap { super.buildUrl( url, params ); - if ( this.portalRequest.getRawPath().startsWith( "/api/" ) ) - { - url.setLength( 0 ); - appendPart( url, RepositoryUtils.getContentRepoName( this.portalRequest.getRepositoryId() ) ); - appendPart( url, this.portalRequest.getBranch().toString() ); - setMustBeRewritten( false ); - } +// if ( this.portalRequest.getRawPath().startsWith( "/api/" ) ) +// { +// url.setLength( 0 ); +// appendPart( url, RepositoryUtils.getContentRepoName( this.portalRequest.getRepositoryId() ) ); +// appendPart( url, this.portalRequest.getBranch().toString() ); +// setMustBeRewritten( false ); +// } final ContentPath resolved = resolvePath(); appendPart( url, resolved.toString() ); diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PathStrategy.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PathStrategy.java new file mode 100644 index 00000000000..0669559c35a --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PathStrategy.java @@ -0,0 +1,6 @@ +package com.enonic.xp.portal.impl.url; + +public interface PathStrategy +{ + String generatePath(); +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlBuilder.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlBuilder.java index 2336b2aae96..406c50f2203 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlBuilder.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlBuilder.java @@ -3,7 +3,6 @@ import java.math.BigInteger; import java.util.Collection; import java.util.Map; -import java.util.Objects; import java.util.UUID; import javax.servlet.http.HttpServletRequest; @@ -38,8 +37,6 @@ abstract class PortalUrlBuilder protected ResourceService resourceService; - protected boolean mustBeRewritten = true; - public final void setParams( final T params ) { this.params = params; @@ -73,21 +70,6 @@ public final String build() } } - protected String getBaseUrl() - { - return null; - } - - protected String getTargetUriPrefix() - { - return null; - } - - public final void setMustBeRewritten( final boolean mustBeRewritten ) - { - this.mustBeRewritten = mustBeRewritten; - } - private String doBuild() { final StringBuilder str = new StringBuilder(); @@ -97,29 +79,7 @@ private String doBuild() buildUrl( str, params ); appendParams( str, params.entries() ); - final String rawPath = portalRequest.getRawPath(); - final boolean isSlashAPI = rawPath.startsWith( "/api/" ); - - if ( isSlashAPI && !mustBeRewritten ) - { - return str.toString(); - } - - final String baseUrl = isSlashAPI ? getBaseUrl() : null; - if ( baseUrl != null ) - { - return UrlTypeConstants.SERVER_RELATIVE.equals( this.params.getType() ) ? str.toString() : baseUrl + str; - } - - String targetUri = str.toString(); - - if ( isSlashAPI ) - { - String targetPrefix = getTargetUriPrefix(); - targetUri = Objects.requireNonNullElse( targetPrefix, "" ) + ( targetUri.startsWith( "/" ) ? targetUri : "/" + targetUri ); - } - - final UriRewritingResult rewritingResult = ServletRequestUrlHelper.rewriteUri( portalRequest.getRawRequest(), targetUri ); + final UriRewritingResult rewritingResult = ServletRequestUrlHelper.rewriteUri( portalRequest.getRawRequest(), str.toString() ); if ( rewritingResult.isOutOfScope() ) { diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl.java index bcbe1391e45..1d8b0e748ef 100644 --- a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl.java +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl.java @@ -18,15 +18,18 @@ import com.enonic.xp.portal.url.AbstractUrlParams; import com.enonic.xp.portal.url.ApiUrlParams; import com.enonic.xp.portal.url.AssetUrlParams; +import com.enonic.xp.portal.url.AttachmentUrlGeneratorParams; import com.enonic.xp.portal.url.AttachmentUrlParams; import com.enonic.xp.portal.url.ComponentUrlParams; import com.enonic.xp.portal.url.GenerateUrlParams; import com.enonic.xp.portal.url.IdentityUrlParams; +import com.enonic.xp.portal.url.ImageUrlGeneratorParams; import com.enonic.xp.portal.url.ImageUrlParams; import com.enonic.xp.portal.url.PageUrlParams; import com.enonic.xp.portal.url.PortalUrlService; import com.enonic.xp.portal.url.ProcessHtmlParams; import com.enonic.xp.portal.url.ServiceUrlParams; +import com.enonic.xp.portal.url.UrlStrategyFacade; import com.enonic.xp.resource.ResourceService; import com.enonic.xp.security.RoleKeys; import com.enonic.xp.security.auth.AuthenticationInfo; @@ -46,6 +49,8 @@ public final class PortalUrlServiceImpl private final RedirectChecksumService redirectChecksumService; + private final UrlStrategyFacade urlStrategyFacade; + private volatile boolean legacyImageServiceEnabled; private volatile boolean legacyAttachmentServiceEnabled; @@ -57,13 +62,15 @@ public final class PortalUrlServiceImpl @Activate public PortalUrlServiceImpl( @Reference final ContentService contentService, @Reference final ResourceService resourceService, @Reference final MacroService macroService, @Reference final StyleDescriptorService styleDescriptorService, - @Reference final RedirectChecksumService redirectChecksumService ) + @Reference final RedirectChecksumService redirectChecksumService, + @Reference final UrlStrategyFacade urlStrategyFacade ) { this.contentService = contentService; this.resourceService = resourceService; this.macroService = macroService; this.styleDescriptorService = styleDescriptorService; this.redirectChecksumService = redirectChecksumService; + this.urlStrategyFacade = urlStrategyFacade; } @Activate @@ -105,19 +112,35 @@ public String componentUrl( final ComponentUrlParams params ) @Override public String imageUrl( final ImageUrlParams params ) { - final ImageUrlBuilder builder = new ImageUrlBuilder(); - builder.setLegacyImageServiceEnabled( this.legacyImageServiceEnabled ); + if ( this.legacyImageServiceEnabled ) + { + return build( new ImageUrlBuilder(), params ); + } + else + { + final ImageUrlGeneratorParams generatorParams = params.isOffline() || params.getPortalRequest() == null + ? urlStrategyFacade.offlineImageUrlParams( params ) + : urlStrategyFacade.requestImageUrlParams( params ); - return build( builder, params ); + return imageUrl( generatorParams ); + } } @Override public String attachmentUrl( final AttachmentUrlParams params ) { - final AttachmentUrlBuilder builder = new AttachmentUrlBuilder(); - builder.setLegacyAttachmentServiceEnabled( this.legacyAttachmentServiceEnabled ); + if ( this.legacyAttachmentServiceEnabled ) + { + return build( new AttachmentUrlBuilder(), params ); + } + else + { + final AttachmentUrlGeneratorParams generatorParams = params.isOffline() || params.getPortalRequest() == null + ? urlStrategyFacade.offlineAttachmentUrlParams( params ) + : urlStrategyFacade.requestAttachmentUrlParams( params ); - return build( builder, params ); + return attachmentUrl( generatorParams ); + } } @Override @@ -158,6 +181,40 @@ public String apiUrl( final ApiUrlParams params ) } } + @Override + public String imageUrl( final ImageUrlGeneratorParams params ) + { + final ImageMediaPathStrategyParams imageMediaPathStrategyParams = ImageMediaPathStrategyParams.create() + .setMedia( params.getMedia() ) + .setProjectName( params.getProjectName() ) + .setBranch( params.getBranch() ) + .setScale( params.getScale() ) + .setBackground( params.getBackground() ) + .setQuality( params.getQuality() ) + .setFilter( params.getFilter() ) + .setFormat( params.getFormat() ) + .setQueryParams( params.getQueryParams() ) + .build(); + + return runWithAdminRole( + () -> UrlGenerator.generateUrl( params.getBaseUrlStrategy(), new ImageMediaPathStrategy( imageMediaPathStrategyParams ) ) ); + } + + @Override + public String attachmentUrl( final AttachmentUrlGeneratorParams params ) + { + final AttachmentMediaPathStrategyParams strategyParams = AttachmentMediaPathStrategyParams.create() + .setMedia( params.getMedia() ) + .setProjectName( params.getProjectName() ) + .setBranch( params.getBranch() ) + .setDownload( params.isDownload() ) + .setQueryParams( params.getQueryParams() ) + .build(); + + return runWithAdminRole( + () -> UrlGenerator.generateUrl( params.getBaseUrlStrategy(), new AttachmentMediaPathStrategy( strategyParams ) ) ); + } + private , P extends AbstractUrlParams> String build( final B builder, final P params ) { builder.setParams( params ); diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/RequestBaseUrlStrategy.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/RequestBaseUrlStrategy.java new file mode 100644 index 00000000000..a612798f3c3 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/RequestBaseUrlStrategy.java @@ -0,0 +1,142 @@ +package com.enonic.xp.portal.impl.url; + +import java.util.Objects; + +import javax.servlet.http.HttpServletRequestWrapper; + +import com.enonic.xp.content.ContentPath; +import com.enonic.xp.content.ContentService; +import com.enonic.xp.context.Context; +import com.enonic.xp.context.ContextAccessor; +import com.enonic.xp.context.ContextBuilder; +import com.enonic.xp.portal.PortalRequest; +import com.enonic.xp.portal.impl.ContentResolver; +import com.enonic.xp.portal.impl.exception.OutOfScopeException; +import com.enonic.xp.portal.url.BaseUrlStrategy; +import com.enonic.xp.portal.url.UrlTypeConstants; +import com.enonic.xp.project.ProjectName; +import com.enonic.xp.site.Site; +import com.enonic.xp.web.servlet.ServletRequestUrlHelper; +import com.enonic.xp.web.servlet.UriRewritingResult; + +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.appendPart; + +final class RequestBaseUrlStrategy + implements BaseUrlStrategy +{ + private final ContentService contentService; + + private final PortalRequest portalRequest; + + private final String urlType; + + private RequestBaseUrlStrategy( final Builder builder ) + { + this.contentService = Objects.requireNonNull( builder.contentService ); + this.portalRequest = Objects.requireNonNull( builder.portalRequest ); + this.urlType = Objects.requireNonNullElse( builder.urlType, UrlTypeConstants.SERVER_RELATIVE ); + } + + @Override + public String generateBaseUrl() + { + final String uri = generateUri(); + + final UriRewritingResult rewritingResult = ServletRequestUrlHelper.rewriteUri( portalRequest.getRawRequest(), uri ); + + if ( rewritingResult.isOutOfScope() ) + { + throw new OutOfScopeException( "URI out of scope" ); + } + + final String rewrittenUri = rewritingResult.getRewrittenUri(); + + if ( UrlTypeConstants.ABSOLUTE.equals( urlType ) ) + { + return ServletRequestUrlHelper.getServerUrl( portalRequest.getRawRequest() ) + rewrittenUri; + } + else if ( UrlTypeConstants.WEBSOCKET.equals( urlType ) ) + { + return ServletRequestUrlHelper.getServerUrl( new HttpServletRequestWrapper( portalRequest.getRawRequest() ) + { + @Override + public String getScheme() + { + return isSecure() ? "wss" : "ws"; + } + } ) + rewrittenUri; + } + else + { + return rewrittenUri; + } + } + + private String generateUri() + { + final StringBuilder uriBuilder = new StringBuilder( portalRequest.getBaseUri() ); + + if ( portalRequest.isSiteBase() ) + { + appendPart( uriBuilder, ProjectName.from( portalRequest.getRepositoryId() ).toString() ); + appendPart( uriBuilder, portalRequest.getBranch().getValue() ); + appendPart( uriBuilder, resolveSitePath().toString() ); + } + + appendPart( uriBuilder, "_" ); + + return uriBuilder.toString(); + } + + private ContentPath resolveSitePath() + { + final Context context = ContextBuilder.copyOf( ContextAccessor.current() ) + .repositoryId( portalRequest.getRepositoryId() ) + .branch( portalRequest.getBranch() ) + .build(); + + Site nearestSite = context.callWith( () -> { + final ContentResolver contentResolver = new ContentResolver( contentService ); + return contentResolver.resolve( portalRequest ).getNearestSite(); + } ); + + return nearestSite != null ? nearestSite.getPath() : ContentPath.ROOT; + } + + public static Builder create() + { + return new Builder(); + } + + static class Builder + { + private ContentService contentService; + + private PortalRequest portalRequest; + + private String urlType; + + public Builder contentService( final ContentService contentService ) + { + this.contentService = contentService; + return this; + } + + public Builder portalRequest( final PortalRequest portalRequest ) + { + this.portalRequest = portalRequest; + return this; + } + + public Builder urlType( final String urlType ) + { + this.urlType = urlType; + return this; + } + + public RequestBaseUrlStrategy build() + { + return new RequestBaseUrlStrategy( this ); + } + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlGenerator.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlGenerator.java new file mode 100644 index 00000000000..5a6920015d7 --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlGenerator.java @@ -0,0 +1,84 @@ +package com.enonic.xp.portal.impl.url; + +import java.math.BigInteger; +import java.util.Objects; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.enonic.xp.exception.NotFoundException; +import com.enonic.xp.portal.impl.exception.OutOfScopeException; +import com.enonic.xp.portal.url.BaseUrlStrategy; + +import static com.enonic.xp.portal.impl.url.UrlBuilderHelper.urlEncode; +import static com.google.common.base.Strings.isNullOrEmpty; + +final class UrlGenerator +{ + private static final Logger LOG = LoggerFactory.getLogger( UrlGenerator.class ); + + public static String generateUrl( final BaseUrlStrategy baseUrlStrategy, final PathStrategy pathStrategy ) + { + String baseUrl = null; + try + { + baseUrl = removeTrailingSlash( baseUrlStrategy.generateBaseUrl() ); + final String path = normalizePath( pathStrategy.generatePath() ); + return baseUrl + path; + } + catch ( Exception e ) + { + return buildErrorUrl( baseUrl, e ); + } + } + + private static String removeTrailingSlash( final String path ) + { + if ( isNullOrEmpty( path ) ) + { + return ""; + } + + return path.endsWith( "/" ) ? path.substring( 0, path.length() - 1 ) : path; + } + + private static String normalizePath( final String path ) + { + if ( isNullOrEmpty( path ) ) + { + return ""; + } + + return !path.startsWith( "/" ) ? "/" + path : path; + } + + private static String buildErrorUrl( final String baseUrl, final Exception e ) + { + final String logRef = LOG.isWarnEnabled() ? newLogRef() : ""; + LOG.warn( "Portal url build failed. Logref: {}", logRef, e ); + + if ( e instanceof NotFoundException ) + { + return buildErrorUrl( baseUrl, 404, String.join( " ", "Not Found.", logRef ) ); + } + else if ( e instanceof OutOfScopeException ) + { + return buildErrorUrl( baseUrl, 400, String.join( " ", "Out of scope.", logRef ) ); + } + else + { + return buildErrorUrl( baseUrl, 500, String.join( " ", "Something went wrong.", logRef ) ); + } + } + + private static String newLogRef() + { + return new BigInteger( UUID.randomUUID().toString().replace( "-", "" ), 16 ).toString( 32 ); + } + + private static String buildErrorUrl( final String baseUrl, final int code, final String message ) + { + return Objects.requireNonNullElse( baseUrl, "/_" ) + "/error/" + code + "?message=" + urlEncode( message ); + } +} diff --git a/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlStrategyFacadeImpl.java b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlStrategyFacadeImpl.java new file mode 100644 index 00000000000..8f1580d53ba --- /dev/null +++ b/modules/portal/portal-impl/src/main/java/com/enonic/xp/portal/impl/url/UrlStrategyFacadeImpl.java @@ -0,0 +1,261 @@ +package com.enonic.xp.portal.impl.url; + +import java.util.Objects; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.enonic.xp.branch.Branch; +import com.enonic.xp.content.Content; +import com.enonic.xp.content.ContentConstants; +import com.enonic.xp.content.ContentId; +import com.enonic.xp.content.ContentPath; +import com.enonic.xp.content.ContentService; +import com.enonic.xp.content.Media; +import com.enonic.xp.context.ContextAccessor; +import com.enonic.xp.context.ContextBuilder; +import com.enonic.xp.exception.NotFoundException; +import com.enonic.xp.portal.PortalRequest; +import com.enonic.xp.portal.url.AttachmentUrlGeneratorParams; +import com.enonic.xp.portal.url.AttachmentUrlParams; +import com.enonic.xp.portal.url.BaseUrlStrategy; +import com.enonic.xp.portal.url.ImageUrlGeneratorParams; +import com.enonic.xp.portal.url.ImageUrlParams; +import com.enonic.xp.portal.url.UrlStrategyFacade; +import com.enonic.xp.project.ProjectName; +import com.enonic.xp.project.ProjectService; +import com.enonic.xp.site.Site; + +@Component(immediate = true, service = UrlStrategyFacade.class) +public class UrlStrategyFacadeImpl + implements UrlStrategyFacade +{ + + private final ContentService contentService; + + private final ProjectService projectService; + + @Activate + public UrlStrategyFacadeImpl( @Reference final ContentService contentService, @Reference final ProjectService projectService ) + { + this.contentService = contentService; + this.projectService = projectService; + } + + @Override + public ImageUrlGeneratorParams offlineImageUrlParams( final ImageUrlParams params ) + { + final ProjectName mediaPathProjectName = offlineProjectName( params.getProjectName() ); + final Branch mediaPathBranch = offlineBranch( params.getBranch() ); + final ProjectName prefixAndBaseUrlProjectName = offlinePrefixAndBaseUrlProjectName( params.getProjectName() ); + final Branch prefixAndBaseUrlBranch = offlinePrefixAndBaseUrlBranch( params.getBranch() ); + + final Media media = resolveMedia( mediaPathProjectName, mediaPathBranch, params.getId(), params.getPath() ); + + final String baseUriKey = params.getBaseUrlKey(); + + final Site site = baseUriKey != null ? offlineNearestSite( prefixAndBaseUrlProjectName, prefixAndBaseUrlBranch, baseUriKey ) : null; + + final BaseUrlStrategy baseUrlStrategy = + offlineBaseUrlStrategy( prefixAndBaseUrlProjectName, prefixAndBaseUrlBranch, site != null ? site : media, params.getType() ); + + return ImageUrlGeneratorParams.create() + .setBaseUrlStrategy( baseUrlStrategy ) + .setMedia( media ) + .setProjectName( mediaPathProjectName ) + .setBranch( mediaPathBranch ) + .setScale( params.getScale() ) + .setFormat( params.getFormat() ) + .setFilter( params.getFilter() ) + .setQuality( params.getQuality() ) + .setBackground( params.getBackground() ) + .addQueryParams( params.getParams().asMap() ) + .build(); + } + + @Override + public ImageUrlGeneratorParams requestImageUrlParams( final ImageUrlParams params ) + { + final PortalRequest portalRequest = params.getPortalRequest(); + final ProjectName mediaPathProjectName = requestProjectName( params.getProjectName(), portalRequest ); + final Branch mediaPathBranch = requestBranch( params.getBranch(), portalRequest ); + final Media media = resolveMedia( mediaPathProjectName, mediaPathBranch, params.getId(), params.getPath() ); + + final BaseUrlStrategy baseUrlStrategy = requestBaseUrlStrategy( portalRequest, params.getType() ); + + return ImageUrlGeneratorParams.create() + .setBaseUrlStrategy( baseUrlStrategy ) + .setMedia( media ) + .setProjectName( mediaPathProjectName ) + .setBranch( mediaPathBranch ) + .setScale( params.getScale() ) + .setFormat( params.getFormat() ) + .setFilter( params.getFilter() ) + .setQuality( params.getQuality() ) + .setBackground( params.getBackground() ) + .addQueryParams( params.getParams().asMap() ) + .build(); + } + + @Override + public AttachmentUrlGeneratorParams offlineAttachmentUrlParams( final AttachmentUrlParams params ) + { + final ProjectName mediaPathProjectName = offlineProjectName( params.getProjectName() ); + final Branch mediaPathBranch = offlineBranch( params.getBranch() ); + final ProjectName prefixAndBaseUrlProjectName = offlinePrefixAndBaseUrlProjectName( params.getProjectName() ); + final Branch prefixAndBaseUrlBranch = offlinePrefixAndBaseUrlBranch( params.getBranch() ); + + final String baseUriKey = params.getBaseUrlKey(); + + final Media media = resolveMedia( mediaPathProjectName, mediaPathBranch, params.getId(), params.getPath() ); + + final Site site = baseUriKey != null ? offlineNearestSite( prefixAndBaseUrlProjectName, prefixAndBaseUrlBranch, baseUriKey ) : null; + + final BaseUrlStrategy baseUrlStrategy = + offlineBaseUrlStrategy( prefixAndBaseUrlProjectName, prefixAndBaseUrlBranch, site != null ? site : media, params.getType() ); + + return AttachmentUrlGeneratorParams.create() + .setBaseUrlStrategy( baseUrlStrategy ) + .setProjectName( mediaPathProjectName ) + .setBranch( mediaPathBranch ) + .setMedia( media ) + .setDownload( params.isDownload() ) + .setName( params.getName() ) + .setLabel( params.getLabel() ) + .addQueryParams( params.getParams().asMap() ) + .build(); + } + + @Override + public AttachmentUrlGeneratorParams requestAttachmentUrlParams( final AttachmentUrlParams params ) + { + final PortalRequest portalRequest = params.getPortalRequest(); + + final ProjectName mediaPathProjectName = requestProjectName( params.getProjectName(), portalRequest ); + + final Branch mediaPathBranch = requestBranch( params.getBranch(), portalRequest ); + + final Media media = resolveMedia( mediaPathProjectName, mediaPathBranch, params.getId(), params.getPath() ); + + final BaseUrlStrategy baseUrlStrategy = requestBaseUrlStrategy( portalRequest, params.getType() ); + + return AttachmentUrlGeneratorParams.create() + .setBaseUrlStrategy( baseUrlStrategy ) + .setProjectName( mediaPathProjectName ) + .setBranch( mediaPathBranch ) + .setMedia( media ) + .setDownload( params.isDownload() ) + .setName( params.getName() ) + .setLabel( params.getLabel() ) + .addQueryParams( params.getParams().asMap() ) + .build(); + } + + private Media resolveMedia( final ProjectName projectName, final Branch branch, final String id, final String path ) + { + return ContextBuilder.copyOf( ContextAccessor.current() ) + .repositoryId( projectName.getRepoId() ) + .branch( branch ) + .build() + .callWith( () -> getMedia( Objects.requireNonNullElse( id, path ) ) ); + } + + private Site offlineNearestSite( final ProjectName projectName, final Branch branch, final String contentKey ) + { + return ContextBuilder.copyOf( ContextAccessor.current() ) + .repositoryId( projectName.getRepoId() ) + .branch( branch ) + .build() + .callWith( () -> contentKey.startsWith( "/" ) + ? contentService.findNearestSiteByPath( ContentPath.from( contentKey ) ) + : contentService.getNearestSite( ContentId.from( contentKey ) ) ); + } + + private BaseUrlStrategy offlineBaseUrlStrategy( final ProjectName projectName, final Branch branch, final Content content, + final String urlType ) + { + return OfflineBaseUrlStrategy.create() + .contentService( contentService ) + .projectService( projectService ) + .projectName( projectName ) + .branch( branch ) + .content( content ) + .urlType( urlType ) + .build(); + } + + private BaseUrlStrategy requestBaseUrlStrategy( final PortalRequest portalRequest, final String urlType ) + { + return RequestBaseUrlStrategy.create().contentService( contentService ).portalRequest( portalRequest ).urlType( urlType ).build(); + } + + private ProjectName offlineProjectName( final String projectName ) + { + return projectName != null + ? ProjectName.from( projectName ) + : ProjectName.from( Objects.requireNonNull( ContextAccessor.current().getRepositoryId(), "Project must be provided" ) ); + } + + private ProjectName requestProjectName( final String projectName, final PortalRequest portalRequest ) + { + return projectName != null + ? ProjectName.from( projectName ) + : ProjectName.from( Objects.requireNonNull( portalRequest.getRepositoryId(), "Project must be provided" ) ); + } + + private ProjectName offlinePrefixAndBaseUrlProjectName( final String projectName ) + { + return ContextAccessor.current().getRepositoryId() != null + ? ProjectName.from( ContextAccessor.current().getRepositoryId() ) + : ProjectName.from( Objects.requireNonNull( projectName, "Project must be provided" ) ); + } + + private Branch offlineBranch( final String branch ) + { + return branch != null + ? Branch.from( branch ) + : Objects.requireNonNullElse( ContextAccessor.current().getBranch(), ContentConstants.BRANCH_MASTER ); + } + + private Branch requestBranch( final String branch, final PortalRequest portalRequest ) + { + return branch != null + ? Branch.from( branch ) + : Objects.requireNonNullElse( portalRequest.getBranch(), ContentConstants.BRANCH_MASTER ); + } + + private Branch offlinePrefixAndBaseUrlBranch( final String branch ) + { + return ContextAccessor.current().getBranch() != null + ? ContextAccessor.current().getBranch() + : Objects.requireNonNullElse( Branch.from( branch ), ContentConstants.BRANCH_MASTER ); + } + + private Content getContent( final String contentKey ) + { + if ( contentKey.startsWith( "/" ) ) + { + return contentService.getByPath( ContentPath.from( contentKey ) ); + } + else + { + return contentService.getById( ContentId.from( contentKey ) ); + } + } + + private Media getMedia( final String contentKey ) + { + Content content = getContent( contentKey ); + + if ( !( content instanceof Media ) ) + { + throw new NotFoundException( String.format( "Content [%s] is not a Media", contentKey ) ) + { + }; + } + + return (Media) content; + } + +} diff --git a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/rendering/TextRendererTest.java b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/rendering/TextRendererTest.java index 84a88074e81..6a5fdad964c 100644 --- a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/rendering/TextRendererTest.java +++ b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/rendering/TextRendererTest.java @@ -40,8 +40,7 @@ public void before() { portalRequest = new PortalRequest(); portalResponse = PortalResponse.create().build(); - service = - new PortalUrlServiceImpl( null, null, new MockMacroService(), new MockStyleDescriptorService(), mock() ); + service = new PortalUrlServiceImpl( null, null, new MockMacroService(), new MockStyleDescriptorService(), mock(), null ); portalRequest.setMode( RenderMode.LIVE ); } diff --git a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/AbstractPortalUrlServiceImplTest.java b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/AbstractPortalUrlServiceImplTest.java index 7a2ad08611e..48dc9f6f5d3 100644 --- a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/AbstractPortalUrlServiceImplTest.java +++ b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/AbstractPortalUrlServiceImplTest.java @@ -14,6 +14,8 @@ import com.enonic.xp.portal.PortalRequest; import com.enonic.xp.portal.impl.PortalConfig; import com.enonic.xp.portal.impl.RedirectChecksumService; +import com.enonic.xp.portal.url.UrlStrategyFacade; +import com.enonic.xp.project.ProjectService; import com.enonic.xp.repository.RepositoryId; import com.enonic.xp.resource.ResourceService; import com.enonic.xp.style.StyleDescriptorService; @@ -69,9 +71,13 @@ public void setup() this.redirectChecksumService = mock( RedirectChecksumService.class ); + ProjectService projectService = mock( ProjectService.class ); + + UrlStrategyFacade urlStrategyFacade = new UrlStrategyFacadeImpl( this.contentService, projectService ); + this.service = new PortalUrlServiceImpl( this.contentService, this.resourceService, new MacroServiceImpl(), this.styleDescriptorService, - this.redirectChecksumService ); + this.redirectChecksumService, urlStrategyFacade ); final PortalConfig portalConfig = mock( PortalConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); when( portalConfig.legacy_imageService_enabled() ).thenReturn( true ); diff --git a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/ImageUrlBuilderTest.java b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/ImageUrlBuilderTest.java index c7d3e210bce..f0cd94bd1e0 100644 --- a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/ImageUrlBuilderTest.java +++ b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/ImageUrlBuilderTest.java @@ -72,7 +72,6 @@ public void init() this.imageUrlParams = new ImageUrlParams().portalRequest( portalRequest ).scale( "testScale" ); urlBuilder = new ImageUrlBuilder(); - urlBuilder.setLegacyImageServiceEnabled( true ); urlBuilder.setParams( imageUrlParams ); urlBuilder.contentService = contentService; @@ -198,8 +197,6 @@ public void testImageUrlForAsMedia() imageUrlParams.scale( "block(310,175)" ); - urlBuilder.setLegacyImageServiceEnabled( false ); - final String url = urlBuilder.build(); assertEquals( "/site/myproject/draft/mysite/_/media/image/myproject:draft/testID:ec25d6e4126c7064f82aaab8b34693fc/block-310-175/testName", @@ -225,8 +222,6 @@ public void testImageUrlForAsMediaWithFallbackToProject() imageUrlParams.scale( "block(310,175)" ); - urlBuilder.setLegacyImageServiceEnabled( false ); - final String url = urlBuilder.build(); assertEquals( "/site/myproject/draft/_/media/image/myproject:draft/testID:ec25d6e4126c7064f82aaab8b34693fc/block-310-175/testName", url ); diff --git a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl_imageUrlTest.java b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl_imageUrlTest.java index e9eea7c8db2..47753f3e670 100644 --- a/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl_imageUrlTest.java +++ b/modules/portal/portal-impl/src/test/java/com/enonic/xp/portal/impl/url/PortalUrlServiceImpl_imageUrlTest.java @@ -3,13 +3,18 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import com.enonic.xp.app.ApplicationKey; import com.enonic.xp.content.Content; import com.enonic.xp.content.ContentConstants; +import com.enonic.xp.content.ContentId; +import com.enonic.xp.content.ContentName; import com.enonic.xp.content.ContentNotFoundException; import com.enonic.xp.content.ContentPath; import com.enonic.xp.content.Media; import com.enonic.xp.context.Context; +import com.enonic.xp.context.ContextAccessor; import com.enonic.xp.context.ContextBuilder; +import com.enonic.xp.data.PropertyTree; import com.enonic.xp.portal.impl.ContentFixtures; import com.enonic.xp.portal.impl.PortalConfig; import com.enonic.xp.portal.url.ContextPathType; @@ -20,9 +25,12 @@ import com.enonic.xp.security.acl.AccessControlEntry; import com.enonic.xp.security.acl.AccessControlList; import com.enonic.xp.site.Site; +import com.enonic.xp.site.SiteConfig; +import com.enonic.xp.site.SiteConfigs; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -37,7 +45,8 @@ public void createUrl() final ImageUrlParams params = new ImageUrlParams().portalRequest( this.portalRequest ).scale( "max(300)" ).validate(); final String url = this.service.imageUrl( params ); - assertEquals( "/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent", url ); + assertEquals( "/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent", + url ); } @Test @@ -63,7 +72,8 @@ public void createUrl_withFormat() new ImageUrlParams().format( "png" ).portalRequest( this.portalRequest ).scale( "max(300)" ).validate(); final String url = this.service.imageUrl( params ); - assertEquals( "/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", url ); + assertEquals( "/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", + url ); } @Test @@ -145,7 +155,8 @@ public void createUrl_absolute() final String url = this.service.imageUrl( params ); assertEquals( - "http://localhost/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent", url ); + "http://localhost/site/myproject/draft/a/b/mycontent/_/image/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent", + url ); } @Test @@ -179,8 +190,9 @@ public void createImageUrlForSlashApiWithVhostContextConfig() .scale( "max(300)" ); String url = this.service.imageUrl( params ); - assertEquals( "http://media.enonic.com/image/myproject:draft/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", - url ); + assertEquals( + "http://media.enonic.com/image/myproject:draft/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", + url ); params = new ImageUrlParams().format( "png" ) .type( UrlTypeConstants.SERVER_RELATIVE ) @@ -193,43 +205,47 @@ public void createImageUrlForSlashApiWithVhostContextConfig() } @Test - public void createUrlForSlashApi() + public void createImageUrlForMasterBranch() { - this.portalRequest.setBaseUri( "" ); - this.portalRequest.setRawPath( "/api/com.enonic.app.appname" ); - this.portalRequest.setContent( createContent() ); + ContextAccessor.current().getLocalScope().setAttribute( RepositoryId.from( "com.enonic.cms.myproject1" ) ); + ContextAccessor.current().getLocalScope().setAttribute( ContentConstants.BRANCH_DRAFT ); - when( req.getServerName() ).thenReturn( "localhost" ); - when( req.getScheme() ).thenReturn( "http" ); - when( req.getServerPort() ).thenReturn( 80 ); + final ImageUrlParams params = new ImageUrlParams().format( "png" ) + .projectName( "myproject2" ) + .branch( "master" ) + .baseUrlKey( "siteId" ) + .id( "123456" ) + .scale( "max(300)" ); - ImageUrlParams params = - new ImageUrlParams().format( "png" ).type( UrlTypeConstants.ABSOLUTE ).portalRequest( this.portalRequest ).scale( "max(300)" ); + final PortalConfig portalConfig = mock( PortalConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); + when( portalConfig.legacy_imageService_enabled() ).thenReturn( false ); + this.service.activate( portalConfig ); - Context context = ContextBuilder.create().build(); - String url = context.callWith( () -> this.service.imageUrl( params ) ); - assertEquals( "http://localhost/api/media/image/myproject:draft/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", - url ); - } + final PropertyTree config = new PropertyTree(); + config.addString( "baseUrl", "https://cdn.company.com" ); - @Test - public void createImageUrlForMasterBranch() - { - this.portalRequest.setBaseUri( "" ); - this.portalRequest.setRawPath( "/api/com.enonic.app.appname" ); - this.portalRequest.setContent( createContent() ); - this.portalRequest.setBranch( ContentConstants.BRANCH_MASTER ); + SiteConfigs siteConfigs = SiteConfigs.create() + .add( SiteConfig.create().application( ApplicationKey.from( "com.enonic.xp.site" ) ).config( config ).build() ) + .build(); - when( req.getServerName() ).thenReturn( "localhost" ); - when( req.getScheme() ).thenReturn( "http" ); - when( req.getServerPort() ).thenReturn( 80 ); + final Site site = mock( Site.class ); + when( site.getPath() ).thenReturn( ContentPath.from( "/mysite" ) ); + when( site.getSiteConfigs() ).thenReturn( siteConfigs ); + when( site.getPermissions() ).thenReturn( + AccessControlList.of( AccessControlEntry.create().principal( RoleKeys.ADMIN ).allowAll().build() ) ); - ImageUrlParams params = - new ImageUrlParams().format( "png" ).type( UrlTypeConstants.ABSOLUTE ).portalRequest( this.portalRequest ).scale( "max(300)" ); + when( contentService.getNearestSite( eq( ContentId.from( "siteId" ) ) ) ).thenReturn( site ); - Context context = ContextBuilder.create().build(); - String url = context.callWith( () -> this.service.imageUrl( params ) ); - assertEquals( "http://localhost/api/media/image/myproject/123456:8cf45815bba82c9711c673c9bb7304039a790026/max-300/mycontent.png", url ); + final Media media = mock( Media.class ); + when( media.getId() ).thenReturn( ContentId.from( "123456" ) ); + when( media.getName() ).thenReturn( ContentName.from( "mycontent.png" ) ); + when( media.getMediaAttachment() ).thenReturn( ContentFixtures.newMedia().getMediaAttachment() ); + when( contentService.getById( eq( ContentId.from( "123456" ) ) ) ).thenReturn( media ); + + final String url = this.service.imageUrl( params ); + assertEquals( + "https://cdn.company.com/site/myproject1/draft/mysite/_/media/image/myproject2/123456:ec25d6e4126c7064f82aaab8b34693fc/max-300/mycontent.png", + url ); } @Test @@ -260,33 +276,78 @@ public void createImageUrlWhenLegacyModeDisabledWithoutSite() @Test public void createImageUrlWhenLegacyModeDisabledWithSite() { - this.portalRequest.setBaseUri( "/site" ); - this.portalRequest.setRawPath( "/site/myproject/draft/a/b/mycontent" ); - this.portalRequest.setContent( createContent() ); + final ImageUrlParams params = new ImageUrlParams().format( "png" ) + .projectName( "myproject" ) + .branch( "draft" ) + .baseUrlKey( "siteId" ) + .id( "123456" ) + .scale( "max(300)" ); - final ImageUrlParams params = - new ImageUrlParams().format( "png" ).type( UrlTypeConstants.ABSOLUTE ).portalRequest( this.portalRequest ).scale( "max(300)" ); + final PortalConfig portalConfig = mock( PortalConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); + when( portalConfig.legacy_imageService_enabled() ).thenReturn( false ); + this.service.activate( portalConfig ); - when( req.getServerName() ).thenReturn( "localhost" ); - when( req.getScheme() ).thenReturn( "http" ); - when( req.getServerPort() ).thenReturn( 8080 ); + final PropertyTree config = new PropertyTree(); + config.addString( "baseUrl", "https://cdn.company.com" ); + + SiteConfigs siteConfigs = SiteConfigs.create() + .add( SiteConfig.create().application( ApplicationKey.from( "com.enonic.xp.site" ) ).config( config ).build() ) + .build(); + + final Site site = mock( Site.class ); + when( site.getPath() ).thenReturn( ContentPath.from( "/mysite" ) ); + when( site.getSiteConfigs() ).thenReturn( siteConfigs ); + when( site.getPermissions() ).thenReturn( + AccessControlList.of( AccessControlEntry.create().principal( RoleKeys.ADMIN ).allowAll().build() ) ); + + when( contentService.getNearestSite( eq( ContentId.from( "siteId" ) ) ) ).thenReturn( site ); + + final Media media = mock( Media.class ); + when( media.getId() ).thenReturn( ContentId.from( "123456" ) ); + when( media.getName() ).thenReturn( ContentName.from( "mycontent.png" ) ); + when( media.getMediaAttachment() ).thenReturn( ContentFixtures.newMedia().getMediaAttachment() ); + when( contentService.getById( eq( ContentId.from( "123456" ) ) ) ).thenReturn( media ); + + final String url = this.service.imageUrl( params ); + assertEquals( + "https://cdn.company.com/site/myproject/draft/mysite/_/media/image/myproject:draft/123456:ec25d6e4126c7064f82aaab8b34693fc/max-300/mycontent.png", + url ); + } + + @Test + public void createImageUrlWhenLegacyModeDisabledSlashApi() + { + final ImageUrlParams params = + new ImageUrlParams().format( "png" ).projectName( "myproject" ).branch( "draft" ).id( "123456" ).scale( "max(300)" ); final PortalConfig portalConfig = mock( PortalConfig.class, invocation -> invocation.getMethod().getDefaultValue() ); when( portalConfig.legacy_imageService_enabled() ).thenReturn( false ); this.service.activate( portalConfig ); + final PropertyTree config = new PropertyTree(); + config.addString( "baseUrl", "https://cdn.company.com" ); + + SiteConfigs siteConfigs = SiteConfigs.create() + .add( SiteConfig.create().application( ApplicationKey.from( "com.enonic.xp.site" ) ).config( config ).build() ) + .build(); + + final Media media = mock( Media.class ); + when( media.getId() ).thenReturn( ContentId.from( "123456" ) ); + when( media.getName() ).thenReturn( ContentName.from( "mycontent.png" ) ); + when( media.getMediaAttachment() ).thenReturn( ContentFixtures.newMedia().getMediaAttachment() ); + when( contentService.getById( eq( ContentId.from( "123456" ) ) ) ).thenReturn( media ); + final Site site = mock( Site.class ); - when( site.getPath() ).thenReturn( ContentPath.from( "/a/b" ) ); + when( site.getPath() ).thenReturn( ContentPath.from( "/mysite" ) ); + when( site.getSiteConfigs() ).thenReturn( siteConfigs ); when( site.getPermissions() ).thenReturn( AccessControlList.of( AccessControlEntry.create().principal( RoleKeys.ADMIN ).allowAll().build() ) ); - when( contentService.getByPath( ContentPath.from( "/a/b" ) ) ).thenReturn( site ); - when( contentService.findNearestSiteByPath( ContentPath.from( "/a/b/mycontent" ) ) ).thenReturn( site ); + when( contentService.getNearestSite( eq( media.getId() ) ) ).thenReturn( site ); final String url = this.service.imageUrl( params ); assertEquals( - "http://localhost:8080/site/myproject/draft/a/b/_/media/image/myproject:draft/123456:ec25d6e4126c7064f82aaab8b34693fc/max-300/mycontent.png", - url ); + "https://cdn.company.com/api/media/image/myproject:draft/123456:ec25d6e4126c7064f82aaab8b34693fc/max-300/mycontent.png", url ); } @Test