@ -19,21 +19,20 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.node.ArrayNode ;
import com.fasterxml.jackson.databind.node.ArrayNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.google.common.hash.Hashing ;
import lombok.Data ;
import lombok.SneakyThrows ;
import lombok.SneakyThrows ;
import lombok.extern.slf4j.Slf4j ;
import lombok.extern.slf4j.Slf4j ;
import org.apache.commons.lang3.StringUtils ;
import org.apache.commons.lang3.tuple.Pair ;
import org.apache.commons.lang3.tuple.Pair ;
import org.springframework.stereotype.Service ;
import org.springframework.stereotype.Service ;
import org.springframework.util.Base64Utils ;
import org.springframework.util.Base64Utils ;
import org.thingsboard.common.util.JacksonUtil ;
import org.thingsboard.common.util.JacksonUtil ;
import org.thingsboard.common.util.RegexUtils ;
import org.thingsboard.server.common.data.Dashboard ;
import org.thingsboard.server.common.data.Dashboard ;
import org.thingsboard.server.common.data.DataConstants ;
import org.thingsboard.server.common.data.DataConstants ;
import org.thingsboard.server.common.data.EntityType ;
import org.thingsboard.server.common.data.EntityType ;
import org.thingsboard.server.common.data.HasImage ;
import org.thingsboard.server.common.data.HasImage ;
import org.thingsboard.server.common.data.ImageDescriptor ;
import org.thingsboard.server.common.data.ImageDescriptor ;
import org.thingsboard.server.common.data.ResourceType ;
import org.thingsboard.server.common.data.ResourceType ;
import org.thingsboard.server.common.data.StringUtils ;
import org.thingsboard.server.common.data.TbImageDeleteResult ;
import org.thingsboard.server.common.data.TbImageDeleteResult ;
import org.thingsboard.server.common.data.TbResource ;
import org.thingsboard.server.common.data.TbResource ;
import org.thingsboard.server.common.data.TbResourceInfo ;
import org.thingsboard.server.common.data.TbResourceInfo ;
@ -66,7 +65,6 @@ import java.util.Iterator;
import java.util.LinkedList ;
import java.util.LinkedList ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.Objects ;
import java.util.Queue ;
import java.util.Queue ;
import java.util.Set ;
import java.util.Set ;
import java.util.regex.Pattern ;
import java.util.regex.Pattern ;
@ -177,19 +175,16 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String basename = StringUtils . substringBeforeLast ( filename , "." ) ;
String basename = StringUtils . substringBeforeLast ( filename , "." ) ;
String extension = StringUtils . substringAfterLast ( filename , "." ) ;
String extension = StringUtils . substringAfterLast ( filename , "." ) ;
Pattern similarImagesPattern = Pattern . compile (
Set < String > existing = resourceInfoDao . findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix (
Pattern . quote ( basename ) + "_(\\d+)\\.?" + Pattern . quote ( extension )
tenantId , ResourceType . IMAGE , basename
) ;
) ;
int maxImageIdx = resourceInfoDao . findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith (
String resourceKey = filename ;
tenantId , ResourceType . IMAGE , basename + "_" ) . stream ( )
int idx = 1 ;
. map ( key - > RegexUtils . getMatch ( key , similarImagesPattern , 1 ) )
while ( existing . contains ( resourceKey ) ) {
. filter ( Objects : : nonNull ) . mapToInt ( Integer : : parseInt )
resourceKey = basename + "_(" + idx + ")." + extension ;
. max ( ) . orElse ( 0 ) ;
idx + + ;
String uniqueKey = basename + "_" + ( maxImageIdx + 1 ) ;
if ( ! extension . isEmpty ( ) ) {
uniqueKey + = "." + extension ;
}
}
return uniqu eKey;
return resourceKey ;
}
}
@Override
@Override
@ -259,47 +254,57 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return result . success ( success ) . build ( ) ;
return result . success ( success ) . build ( ) ;
}
}
@Override
public List < TbResourceInfo > findSimilarImagesByTenantIdAndKeyStartingWith ( TenantId tenantId , byte [ ] data , String imageKeyStartingWith ) {
String etag = calculateEtag ( data ) ;
return resourceInfoDao . findByTenantIdAndEtagAndKeyStartingWith ( tenantId , etag , imageKeyStartingWith ) ;
}
@Override
@Override
public TbResourceInfo findImageByTenantIdAndEtag ( TenantId tenantId , String etag ) {
public TbResourceInfo findImageByTenantIdAndEtag ( TenantId tenantId , String etag ) {
return resourceInfoDao . findByTenantIdAndEtag ( tenantId , ResourceType . IMAGE , etag ) ;
return resourceInfoDao . findByTenantIdAndEtag ( tenantId , ResourceType . IMAGE , etag ) ;
}
}
@Override
@Override
public boolean replaceBase64WithImageUrl ( HasImage entity , String title , String type ) {
public boolean replaceBase64WithImageUrl ( HasImage entity , String type ) {
entity . setImage ( base64ToImageUrl ( entity . getTenantId ( ) , "\"" + title + "\" " + type + " image" , entity . getImage ( ) ) ) ;
String imageName = "\"" + entity . getName ( ) + "\" " ;
return true ; //TODO: should return true only if something is changed.
if ( entity . getTenantId ( ) = = null | | entity . getTenantId ( ) . isSysTenantId ( ) ) {
imageName + = "system " ;
}
imageName = imageName + type + " image" ;
UpdateResult result = base64ToImageUrl ( entity . getTenantId ( ) , imageName , entity . getImage ( ) ) ;
entity . setImage ( result . getValue ( ) ) ;
return result . isUpdated ( ) ;
}
}
@Override
@Override
public boolean replaceBase64WithImageUrl ( WidgetTypeDetails entity ) {
public boolean replaceBase64WithImageUrl ( WidgetTypeDetails entity ) {
String prefix = "\"" + entity . getName ( ) + "\" widget" ; // TODO: "system widget" if SYS TENANT ID;
String prefix = "\"" + entity . getName ( ) + "\" " ;
entity . setImage ( base64ToImageUrl ( entity . getTenantId ( ) , prefix + " image" , entity . getImage ( ) ) ) ;
if ( entity . getTenantId ( ) = = null | | entity . getTenantId ( ) . isSysTenantId ( ) ) {
prefix + = "system " ;
}
prefix + = "widget" ;
UpdateResult result = base64ToImageUrl ( entity . getTenantId ( ) , prefix + " image" , entity . getImage ( ) ) ;
entity . setImage ( result . getValue ( ) ) ;
boolean updated = result . isUpdated ( ) ;
if ( entity . getDescriptor ( ) . isObject ( ) ) {
if ( entity . getDescriptor ( ) . isObject ( ) ) {
ObjectNode descriptor = ( ObjectNode ) entity . getDescriptor ( ) ;
ObjectNode descriptor = ( ObjectNode ) entity . getDescriptor ( ) ;
JsonNode defaultConfig = JacksonUtil . toJsonNode ( descriptor . get ( "defaultConfig" ) . asText ( ) ) ;
JsonNode defaultConfig = JacksonUtil . toJsonNode ( descriptor . get ( "defaultConfig" ) . asText ( ) ) ;
base64ToImageUrlUsingMapping ( entity . getTenantId ( ) , WIDGET_TYPE_BASE64_MAPPING , Collections . singletonMap ( "prefix" , prefix ) , defaultConfig ) ;
updated | = base64ToImageUrlUsingMapping ( entity . getTenantId ( ) , WIDGET_TYPE_BASE64_MAPPING , Collections . singletonMap ( "prefix" , prefix ) , defaultConfig ) ;
descriptor . put ( "defaultConfig" , defaultConfig . toString ( ) ) ;
descriptor . put ( "defaultConfig" , defaultConfig . toString ( ) ) ;
}
}
base64ToImageUrlRecursively ( entity . getTenantId ( ) , prefix , entity . getDescriptor ( ) ) ;
updated | = base64ToImageUrlRecursively ( entity . getTenantId ( ) , prefix , entity . getDescriptor ( ) ) ;
return true ; //TODO: should return true only if something is changed.
return updated ;
}
}
@Override
@Override
public boolean replaceBase64WithImageUrl ( Dashboard entity ) {
public boolean replaceBase64WithImageUrl ( Dashboard entity ) {
String prefix = "\"" + entity . getTitle ( ) + "\" dashboard" ;
String prefix = "\"" + entity . getTitle ( ) + "\" dashboard" ;
entity . setImage ( base64ToImageUrl ( entity . getTenantId ( ) , prefix + " image" , entity . getImage ( ) ) ) ;
var result = base64ToImageUrl ( entity . getTenantId ( ) , prefix + " image" , entity . getImage ( ) ) ;
base64ToImageUrlUsingMapping ( entity . getTenantId ( ) , DASHBOARD_BASE64_MAPPING , Collections . singletonMap ( "prefix" , prefix ) , entity . getConfiguration ( ) ) ;
boolean updated = result . isUpdated ( ) ;
base64ToImageUrlRecursively ( entity . getTenantId ( ) , prefix , entity . getConfiguration ( ) ) ;
entity . setImage ( result . getValue ( ) ) ;
return true ; //TODO: should return true only if something is changed.
updated | = base64ToImageUrlUsingMapping ( entity . getTenantId ( ) , DASHBOARD_BASE64_MAPPING , Collections . singletonMap ( "prefix" , prefix ) , entity . getConfiguration ( ) ) ;
updated | = base64ToImageUrlRecursively ( entity . getTenantId ( ) , prefix , entity . getConfiguration ( ) ) ;
return updated ;
}
}
private void base64ToImageUrlUsingMapping ( TenantId tenantId , Map < String , String > mapping , Map < String , String > templateParams , JsonNode configuration ) {
private boolean base64ToImageUrlUsingMapping ( TenantId tenantId , Map < String , String > mapping , Map < String , String > templateParams , JsonNode configuration ) {
boolean updated = false ;
for ( var entry : mapping . entrySet ( ) ) {
for ( var entry : mapping . entrySet ( ) ) {
String expression = entry . getValue ( ) ;
String expression = entry . getValue ( ) ;
Queue < JsonPathProcessingTask > tasks = new LinkedList < > ( ) ;
Queue < JsonPathProcessingTask > tasks = new LinkedList < > ( ) ;
@ -330,8 +335,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String variableName = null ;
String variableName = null ;
String variableValue = null ;
String variableValue = null ;
if ( token . contains ( "[$" ) ) {
if ( token . contains ( "[$" ) ) {
variableName = org . apache . commons . lang3 . StringUtils . substringBetween ( token , "[$" , "]" ) ;
variableName = StringUtils . substringBetween ( token , "[$" , "]" ) ;
token = org . apache . commons . lang3 . StringUtils . substringBefore ( token , "[$" ) ;
token = StringUtils . substringBefore ( token , "[$" ) ;
}
}
if ( node . has ( token ) ) {
if ( node . has ( token ) ) {
JsonNode value = node . get ( token ) ;
JsonNode value = node . get ( token ) ;
@ -344,16 +349,20 @@ public class BaseImageService extends BaseResourceService implements ImageServic
name = name . replace ( "$" + replacements . getKey ( ) , replacements . getValue ( ) ) ;
name = name . replace ( "$" + replacements . getKey ( ) , replacements . getValue ( ) ) ;
}
}
if ( node . isObject ( ) & & value . isTextual ( ) ) {
if ( node . isObject ( ) & & value . isTextual ( ) ) {
( ( ObjectNode ) node ) . put ( token , base64ToImageUrl ( tenantId , name , value . asText ( ) ) ) ;
var result = base64ToImageUrl ( tenantId , name , value . asText ( ) ) ;
( ( ObjectNode ) node ) . put ( token , result . getValue ( ) ) ;
updated | = result . isUpdated ( ) ;
} else if ( value . isArray ( ) ) {
} else if ( value . isArray ( ) ) {
ArrayNode array = ( ArrayNode ) value ;
ArrayNode array = ( ArrayNode ) value ;
for ( int i = 0 ; i < array . size ( ) ; i + + ) {
for ( int i = 0 ; i < array . size ( ) ; i + + ) {
String arrayElementName = name . replace ( "$index" , Integer . toString ( i ) ) ;
String arrayElementName = name . replace ( "$index" , Integer . toString ( i ) ) ;
array . set ( i , base64ToImageUrl ( tenantId , arrayElementName , array . get ( i ) . asText ( ) ) ) ;
UpdateResult result = base64ToImageUrl ( tenantId , arrayElementName , array . get ( i ) . asText ( ) ) ;
array . set ( i , result . getValue ( ) ) ;
updated | = result . isUpdated ( ) ;
}
}
}
}
} else {
} else {
if ( org . thingsboard . server . common . data . StringUtils . isNotEmpty ( variableName ) & & org . thingsboard . server . common . data . StringUtils . isNotEmpty ( variableValue ) ) {
if ( StringUtils . isNotEmpty ( variableName ) & & StringUtils . isNotEmpty ( variableValue ) ) {
tasks . add ( task . next ( value , variableName , variableValue ) ) ;
tasks . add ( task . next ( value , variableName , variableValue ) ) ;
} else {
} else {
tasks . add ( task . next ( value ) ) ;
tasks . add ( task . next ( value ) ) ;
@ -363,17 +372,18 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
}
}
}
}
}
return updated ;
}
}
private String base64ToImageUrl ( TenantId tenantId , String name , String data ) {
private UpdateResult base64ToImageUrl ( TenantId tenantId , String name , String data ) {
return base64ToImageUrl ( tenantId , name , data , false ) ;
return base64ToImageUrl ( tenantId , name , data , false ) ;
}
}
private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern . compile ( "^tb-image:(.*):(.*);data:(.*);.*" ) ;
private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern . compile ( "^tb-image:(.*):(.*);data:(.*);.*" ) ;
private String base64ToImageUrl ( TenantId tenantId , String name , String data , boolean strict ) {
private UpdateResult base64ToImageUrl ( TenantId tenantId , String name , String data , boolean strict ) {
if ( StringUtils . isBlank ( data ) ) {
if ( StringUtils . isBlank ( data ) ) {
return data ;
return UpdateResult . of ( false , data ) ;
}
}
var matcher = TB_IMAGE_METADATA_PATTERN . matcher ( data ) ;
var matcher = TB_IMAGE_METADATA_PATTERN . matcher ( data ) ;
boolean matches = matcher . matches ( ) ;
boolean matches = matcher . matches ( ) ;
@ -385,67 +395,58 @@ public class BaseImageService extends BaseResourceService implements ImageServic
mdResourceName = new String ( Base64Utils . decodeFromString ( matcher . group ( 2 ) ) , StandardCharsets . UTF_8 ) ;
mdResourceName = new String ( Base64Utils . decodeFromString ( matcher . group ( 2 ) ) , StandardCharsets . UTF_8 ) ;
mdMediaType = matcher . group ( 3 ) ;
mdMediaType = matcher . group ( 3 ) ;
} else if ( data . startsWith ( DataConstants . TB_IMAGE_PREFIX + "data:image/" ) | | ( ! strict & & data . startsWith ( "data:image/" ) ) ) {
} else if ( data . startsWith ( DataConstants . TB_IMAGE_PREFIX + "data:image/" ) | | ( ! strict & & data . startsWith ( "data:image/" ) ) ) {
mdMediaType = org . apache . commons . lang3 . StringUtils . substringBetween ( data , "data:" , ";base64" ) ;
mdMediaType = StringUtils . substringBetween ( data , "data:" , ";base64" ) ;
} else {
} else {
return data ;
return UpdateResult . of ( false , data ) ;
}
}
String base64Data = org . apache . commons . lang3 . StringUtils . substringAfter ( data , "base64," ) ;
String base64Data = StringUtils . substringAfter ( data , "base64," ) ;
String extension = ImageUtils . mediaTypeToFileExtension ( mdMediaType ) ;
String extension = ImageUtils . mediaTypeToFileExtension ( mdMediaType ) ;
byte [ ] imageData = Base64 . getDecoder ( ) . decode ( base64Data ) ;
byte [ ] imageData = Base64 . getDecoder ( ) . decode ( base64Data ) ;
String etag = Hashing . sha256 ( ) . hashBytes ( imageData ) . toString ( ) ;
String etag = calculateEtag ( imageData ) ;
var imageInfo = findImageByTenantIdAndEtag ( tenantId , etag ) ;
var imageInfo = findImageByTenantIdAndEtag ( tenantId , etag ) ;
if ( imageInfo = = null ) {
if ( imageInfo = = null ) {
TbResource image = new TbResource ( ) ;
TbResource image = new TbResource ( ) ;
image . setTenantId ( tenantId ) ;
image . setTenantId ( tenantId ) ;
image . setResourceType ( ResourceType . IMAGE ) ;
image . setResourceType ( ResourceType . IMAGE ) ;
if ( StringUtils . isBlank ( mdResourceName ) ) {
if ( StringUtils . isBlank ( mdResourceName ) ) {
mdResourceName = name ;
mdResourceName = name ;
}
}
image . setTitle ( mdResourceName ) ;
String fileName ;
String fileName ;
if ( StringUtils . isBlank ( mdResourceKey ) ) {
if ( StringUtils . isBlank ( mdResourceKey ) ) {
fileName = mdResourceName . toLowerCase ( )
fileName = mdResourceName . toLowerCase ( )
//TODO: improve to list all the special characters via regexp or similar
. replace ( "'" , "" ) . replace ( "\"" , "" )
. replace ( "'" , "" )
. replace ( " " , "_" ) . replace ( "/" , "_" )
. replace ( "\"" , "" )
+ "." + extension ;
. replace ( " " , "_" )
. replace ( "/" , "_" ) ;
mdResourceKey = fileName + "." + extension ;
} else {
} else {
fileName = mdResourceKey . split ( "\\." ) [ 0 ] ;
fileName = mdResourceKey ;
}
Set < String > existingKeys = findResourceKeysByTenantIdResourceTypeAndKeyPrefix ( tenantId , ResourceType . IMAGE , mdResourceKey ) ;
int idx = 1 ;
while ( existingKeys . contains ( mdResourceKey ) ) {
mdResourceKey = fileName + "_(" + idx + ")." + extension ;
idx + + ;
}
}
image . setResourceKey ( mdResourceKey ) ;
image . setFileName ( fileName ) ;
image . setTitle ( mdResourceName ) ;
image . setFileName ( mdResourceKey ) ;
image . setDescriptor ( JacksonUtil . newObjectNode ( ) . put ( "mediaType" , mdMediaType ) ) ;
image . setDescriptor ( JacksonUtil . newObjectNode ( ) . put ( "mediaType" , mdMediaType ) ) ;
image . setData ( imageData ) ;
image . setData ( imageData ) ;
imageInfo = saveImage ( image ) ;
imageInfo = saveImage ( image ) ;
}
}
return DataConstants . TB_IMAGE_PREFIX + imageInfo . getLink ( ) ;
return UpdateResult . of ( true , DataConstants . TB_IMAGE_PREFIX + imageInfo . getLink ( ) ) ;
}
}
private void base64ToImageUrlRecursively ( TenantId tenantId , String title , JsonNode root ) {
private boolean base64ToImageUrlRecursively ( TenantId tenantId , String title , JsonNode root ) {
boolean updated = false ;
Queue < JsonNodeProcessingTask > tasks = new LinkedList < > ( ) ;
Queue < JsonNodeProcessingTask > tasks = new LinkedList < > ( ) ;
tasks . add ( new JsonNodeProcessingTask ( title , root ) ) ;
tasks . add ( new JsonNodeProcessingTask ( title , root ) ) ;
while ( ! tasks . isEmpty ( ) ) {
while ( ! tasks . isEmpty ( ) ) {
JsonNodeProcessingTask task = tasks . poll ( ) ;
JsonNodeProcessingTask task = tasks . poll ( ) ;
JsonNode node = task . getNode ( ) ;
JsonNode node = task . getNode ( ) ;
String currentPath = org . thingsboard . server . common . data . StringUtils . isBlank ( task . getPath ( ) ) ? "" : ( task . getPath ( ) + " " ) ;
String currentPath = StringUtils . isBlank ( task . getPath ( ) ) ? "" : ( task . getPath ( ) + " " ) ;
if ( node . isObject ( ) ) {
if ( node . isObject ( ) ) {
ObjectNode on = ( ObjectNode ) node ;
ObjectNode on = ( ObjectNode ) node ;
for ( Iterator < String > it = on . fieldNames ( ) ; it . hasNext ( ) ; ) {
for ( Iterator < String > it = on . fieldNames ( ) ; it . hasNext ( ) ; ) {
String childName = it . next ( ) ;
String childName = it . next ( ) ;
JsonNode childValue = on . get ( childName ) ;
JsonNode childValue = on . get ( childName ) ;
if ( childValue . isTextual ( ) ) {
if ( childValue . isTextual ( ) ) {
on . put ( childName , base64ToImageUrl ( tenantId , currentPath + childName , childValue . asText ( ) , true ) ) ;
UpdateResult result = base64ToImageUrl ( tenantId , currentPath + childName , childValue . asText ( ) , true ) ;
on . put ( childName , result . getValue ( ) ) ;
updated | = result . isUpdated ( ) ;
} else if ( childValue . isObject ( ) | | childValue . isArray ( ) ) {
} else if ( childValue . isObject ( ) | | childValue . isArray ( ) ) {
tasks . add ( new JsonNodeProcessingTask ( currentPath + childName , childValue ) ) ;
tasks . add ( new JsonNodeProcessingTask ( currentPath + childName , childValue ) ) ;
}
}
@ -461,6 +462,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
}
}
}
}
}
return updated ;
}
}
@Override
@Override
@ -486,7 +488,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
while ( ! tasks . isEmpty ( ) ) {
while ( ! tasks . isEmpty ( ) ) {
JsonNodeProcessingTask task = tasks . poll ( ) ;
JsonNodeProcessingTask task = tasks . poll ( ) ;
JsonNode node = task . getNode ( ) ;
JsonNode node = task . getNode ( ) ;
String currentPath = org . thingsboard . server . common . data . StringUtils . isBlank ( task . getPath ( ) ) ? "" : ( task . getPath ( ) + "." ) ;
String currentPath = StringUtils . isBlank ( task . getPath ( ) ) ? "" : ( task . getPath ( ) + "." ) ;
if ( node . isObject ( ) ) {
if ( node . isObject ( ) ) {
ObjectNode on = ( ObjectNode ) node ;
ObjectNode on = ( ObjectNode ) node ;
for ( Iterator < String > it = on . fieldNames ( ) ; it . hasNext ( ) ; ) {
for ( Iterator < String > it = on . fieldNames ( ) ; it . hasNext ( ) ; ) {
@ -536,7 +538,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
}
private ImageCacheKey getKeyFromUrl ( TenantId tenantId , String url ) {
private ImageCacheKey getKeyFromUrl ( TenantId tenantId , String url ) {
if ( org . thingsboard . server . common . data . StringUtils . isBlank ( url ) ) {
if ( StringUtils . isBlank ( url ) ) {
return null ;
return null ;
}
}
TenantId imageTenantId = null ;
TenantId imageTenantId = null ;
@ -555,4 +557,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
}
return null ;
return null ;
}
}
@Data ( staticConstructor = "of" )
private static class UpdateResult {
private final boolean updated ;
private final String value ;
}
}
}