@@ -535,12 +535,12 @@ public Map<String, Boolean> getSearchableProperties() {
535535 }
536536
537537 public void setSearchableProperties (final Map <String , Boolean > searchableProperties ) {
538- Map < String , Boolean > generatedProperties = generateAndValidateAliases ( searchableProperties );
539-
540- if (! validateSearchableProperties ( generatedProperties ))
541- throw new IllegalArgumentException ( "Searchable Property definition includes one or more malformed entries" );
538+ if ( searchableProperties == null || searchableProperties . isEmpty ()) {
539+ this . searchableProperties = searchableProperties ;
540+ return ;
541+ }
542542
543- this .searchableProperties = generatedProperties ;
543+ this .searchableProperties = normalizeSearchableProperties ( searchableProperties ) ;
544544 }
545545
546546 public Space withSearchableProperties (final Map <String , Boolean > searchableProperties ) {
@@ -561,69 +561,6 @@ public Space withSortableProperties(final List<List<Object>> sortableProperties)
561561 return this ;
562562 }
563563
564- private boolean validateSearchableProperties (final Map <String , Boolean > searchableProperties ) {
565- for (String str : searchableProperties .keySet ()) {
566- if (!str .contains ("::" ))
567- return false ;
568-
569- String [] parts = str .split ("::" , 2 );
570- if (parts .length != 2 )
571- return false ;
572-
573- String jsonPath = parts [0 ];
574- JsonPathValidator .ValidationResult result ;
575- if (str .startsWith ("$" )){
576- String [] subParts = jsonPath .split (":" , 2 );
577- String expression = subParts [1 ].trim ();
578-
579- if (expression .startsWith ("[" ) && expression .endsWith ("]" ))
580- expression = expression .substring (1 , expression .length () - 1 ).trim ();
581-
582- result = JsonPathValidator .validate (expression );
583- }
584- else {
585- result = JsonPathValidator .validate (jsonPath );
586- }
587-
588- if (!result .isValid ()) {
589- throw new IllegalArgumentException (String .format ("Invalid character at position %d for JsonPath string" , result .errorPosition ().orElse (-1 )));
590- }
591-
592- String resultType = parts [1 ];
593- if (!resultType .equals ("scalar" ) && !resultType .equals ("array" ))
594- return false ;
595- }
596-
597- return true ;
598- }
599-
600- private Map <String , Boolean > generateAndValidateAliases (Map <String , Boolean > searchableProperties ) {
601- Map <String , Boolean > generated = new HashMap <>();
602- Set <String > aliases = new HashSet <>();
603-
604- for (Map .Entry <String , Boolean > entry : searchableProperties .entrySet ()) {
605- String alias ;
606- if (!entry .getKey ().startsWith ("$" )) {
607- String [] parts = entry .getKey ().split ("::" , 2 );
608-
609- if (parts .length > 1 ) {
610- alias = "$" + parts [0 ] + ":[$." + entry .getKey () +"]" ;
611- } else
612- alias = "$" + parts [0 ] + ":[$." + entry .getKey () + "]::scalar" ;
613- } else {
614- alias = entry .getKey ();
615- }
616-
617- if (!aliases .add (alias )) {
618- throw new IllegalArgumentException ("Duplicate alias detected: " + alias );
619- }
620-
621- generated .put (alias , entry .getValue ());
622- }
623-
624- return generated ;
625- }
626-
627564 public Map <String , Tag > getTags () {
628565 return tags ;
629566 }
@@ -678,6 +615,167 @@ public Space withMimeType(String mimeType) {
678615 return this ;
679616 }
680617
618+ /**
619+ * Normalizes all searchable property definitions into the canonical form:
620+ * $<alias>:[<jsonPathExpression>]::scalar|array
621+ */
622+ private Map <String , Boolean > normalizeSearchableProperties (Map <String , Boolean > rawProps ) {
623+ Map <String , Boolean > normalized = new LinkedHashMap <>();
624+ Set <String > aliases = new HashSet <>();
625+
626+ for (Map .Entry <String , Boolean > entry : rawProps .entrySet ()) {
627+ String rawKey = entry .getKey ();
628+ Boolean value = entry .getValue ();
629+
630+ if (rawKey == null || rawKey .trim ().isEmpty ()) {
631+ throw new IllegalArgumentException ("Searchable property key must not be null or empty" );
632+ }
633+
634+ NormalizedProperty np = parseAndNormalizeKey (rawKey .trim ());
635+
636+ // Enforce alias uniqueness
637+ if (!aliases .add (np .alias )) {
638+ throw new IllegalArgumentException ("Duplicate alias detected: " + np .alias );
639+ }
640+
641+ String canonicalKey = buildCanonicalKey (np );
642+ normalized .put (canonicalKey , value );
643+ }
644+
645+ return normalized ;
646+ }
647+
648+ private String buildCanonicalKey (NormalizedProperty np ) {
649+ return "$" + np .alias + ":[" + np .jsonPathExpression + "]::" + np .resultType ;
650+ }
651+
652+ private static class NormalizedProperty {
653+ String alias ;
654+ String jsonPathExpression ;
655+ String resultType ;
656+ }
657+
658+ /**
659+ * Parse a raw key and normalize it to (alias, jsonPathExpression, resultType).
660+ * - New-style keys: "$alias:[$.path]::scalar" (or without [])
661+ * - Legacy keys: "path", "path::scalar", "path::array"
662+ */
663+ private NormalizedProperty parseAndNormalizeKey (String key ) {
664+ if (key .startsWith ("$" ) && key .contains ("::" )) {
665+ NormalizedProperty np = tryParseNewStyleKey (key );
666+ if (np != null ) {
667+ return np ;
668+ }
669+ }
670+
671+ // Fallback
672+ return parseLegacyKey (key );
673+ }
674+
675+ /**
676+ * Parses a new-style key of form
677+ * $alias:[$.jsonPath]::scalar|array
678+ */
679+ private NormalizedProperty tryParseNewStyleKey (String key ) {
680+ String [] typeSplit = key .split ("::" , 2 );
681+ if (typeSplit .length != 2 ) {
682+ return null ;
683+ }
684+
685+ String leftPart = typeSplit [0 ].trim ();
686+ String typePart = typeSplit [1 ].trim ();
687+
688+ if (!"scalar" .equals (typePart ) && !"array" .equals (typePart )) {
689+ return null ;
690+ }
691+
692+ int colonIdx = leftPart .indexOf (':' );
693+ if (colonIdx <= 1 ) {
694+ return null ;
695+ }
696+
697+ String aliasPart = leftPart .substring (1 , colonIdx ).trim ();
698+ String exprPart = leftPart .substring (colonIdx + 1 ).trim ();
699+
700+ if (aliasPart .isEmpty () || exprPart .isEmpty ()) {
701+ return null ;
702+ }
703+
704+ String expression = exprPart ;
705+ if (expression .startsWith ("[" ) && expression .endsWith ("]" ) && expression .length () >= 2 ) {
706+ expression = expression .substring (1 , expression .length () - 1 ).trim ();
707+ }
708+
709+ // Validate JSONPath on the stripped expression
710+ validateJsonPath (expression );
711+
712+ NormalizedProperty np = new NormalizedProperty ();
713+ np .alias = aliasPart ;
714+ np .jsonPathExpression = expression ;
715+ np .resultType = typePart ;
716+ return np ;
717+ }
718+
719+ /**
720+ * Parses legacy keys like:
721+ * - "foo"
722+ * - "foo::array"
723+ */
724+ private NormalizedProperty parseLegacyKey (String key ) {
725+ String base = key ;
726+ String resultType = "scalar" ; //default
727+
728+ int sepIdx = key .lastIndexOf ("::" );
729+ if (sepIdx > -1 && sepIdx + 2 < key .length ()) {
730+ String maybeType = key .substring (sepIdx + 2 ).trim ();
731+ if ("scalar" .equals (maybeType ) || "array" .equals (maybeType )) {
732+ base = key .substring (0 , sepIdx ).trim ();
733+ resultType = maybeType ;
734+ }
735+ }
736+
737+ if (base .isEmpty ()) {
738+ throw new IllegalArgumentException ("Malformed searchable property key: '" + key + "'" );
739+ }
740+
741+ String expression ;
742+ if (base .startsWith ("$" )) {
743+ expression = base ;
744+ }
745+ else {
746+ expression = "$." + base ;
747+ }
748+
749+ // Derive alias from the path (without the leading '$' or '$.')
750+ String alias ;
751+ if (expression .startsWith ("$." ) && expression .length () > 2 ) {
752+ alias = expression .substring (2 );
753+ }
754+ else if (expression .startsWith ("$" ) && expression .length () > 1 ) {
755+ alias = expression .substring (1 );
756+ }
757+ else {
758+ alias = base ;
759+ }
760+
761+ validateJsonPath (expression );
762+
763+ NormalizedProperty np = new NormalizedProperty ();
764+ np .alias = alias ;
765+ np .jsonPathExpression = expression ;
766+ np .resultType = resultType ;
767+ return np ;
768+ }
769+
770+ private void validateJsonPath (String expression ) {
771+ JsonPathValidator .ValidationResult result = JsonPathValidator .validate (expression );
772+ if (!result .isValid ()) {
773+ int pos = result .errorPosition ().orElse (-1 );
774+ throw new IllegalArgumentException (
775+ String .format ("Invalid JSONPath expression '%s'. Error at position %d." , expression , pos ));
776+ }
777+ }
778+
681779 /**
682780 * Used as a JsonView on a {@link Space} to indicate that a property should be part of a response which was requested to contain
683781 * connector information.
0 commit comments