Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search criteria parsing: operator prefixes should not be extracted regardless of the type of the parameter #60

Closed
michelemottini opened this issue Dec 17, 2015 · 8 comments
Labels
Projects

Comments

@michelemottini
Copy link

...?patient=ebda26f3-437a-4c08-a2d5-fe3b0472ddd3 is parsed as 'patient EB da26f3-437a-4c08-a2d5-fe3b0472ddd3' instead than 'patient EQ ebda26f3-437a-4c08-a2d5-fe3b0472ddd3' (etc.)

@michelemottini michelemottini changed the title Search criteria parsing: operator prefixes should not be extracted regardless of the type of the operator Search criteria parsing: operator prefixes should not be extracted regardless of the type of the parameter Dec 17, 2015
@brianpos
Copy link
Contributor

Stumbled on this one too.

@ewoutkramer
Copy link
Member

...but of course we do not have the type of the parameter, since this would require us to feed search param metadata to the parser :-(

So the only solution is to remove this functionality completely?

@michelemottini
Copy link
Author

I am splitting the functionality: leave a simplified parsing and then add various type-specific methods GetStringOpearatorAndValue(), GetTokenOperatorAndValues() that handle the type-specific part of the parsing

I can post here the final result, but unfortunately it means in any case having to modify all the call sites in the search code - the new class won't be compatible with the old one

@ewoutkramer ewoutkramer added the bug label Jan 5, 2016
@ewoutkramer
Copy link
Member

I see this is posted in Spark, but it might well be an API issue. @cknaap do you know which subsystem parses this?

@michelemottini
Copy link
Author

It is Criterium.cs that is in Spark now (it used to be in the Fhir.Net library previously)

@michelemottini
Copy link
Author

This is the solution I came up with - the new Criterium class has methods GetNumberOperatorAndValues(), GetDateOperatorAndValues() etc. that do the type-specific parsing:

    public abstract class Criterium
    {
        protected Criterium( string paramName, string modifier )
        {
            if ( String.IsNullOrEmpty( paramName ) ) throw new ArgumentNullException( nameof( paramName ) );

            ParamName = paramName;
            _modifier = modifier;
        }

        public string ParamName { get; private set; }

        public override string ToString()
        {
            return String.IsNullOrEmpty( _modifier ) ?
                ParamName :
                ParamName + FhirRest.SearchParams.SEARCH_MODIFIERSEPARATOR + _modifier;
        }

        /// <summary>
        /// Gets the chained search criterium from the main search criterium
        /// E.G. if criterium is 'subject:Patient.identifier=xxx' and TReference is Patient returns 'identifier=xxx'
        /// </summary>
        /// <typeparam name="T">The type of the resources being searched</typeparam>
        /// <typeparam name="TReference">The type of the resources that must be referenced by the chained criterium</typeparam>
        /// <returns>Chained search criterium.</returns>
        /// <exception cref="FhirInvalidSearchCriteriaException">Thrown if the search criterium specifies referenced type(s) 
        /// different from TReference</exception>
        /// <exception cref="FhirReferenceTypeNotSupportedException">Thrown if the search criterium specifies referenced type(s) 
        /// different from TReference of if the type modifier does not match any of the possible referenced types</exception>
        /// <exception cref="FhirUnknownSearchCriteriaException">Thrown if no possible referenced types support the specified 
        /// chained search criterium</exception>
        public abstract Criterium GetChainedCriterium<T, TReference>()
            where T : FhirModel.Resource
            where TReference : FhirModel.Resource;

        public abstract OperatorAndValues<decimal> GetNumberOperatorAndValues();

        public abstract OperatorAndValues<DateValue> GetDateOperatorAndValues();

        public abstract OperatorAndValues<string> GetStringOperatorAndValues();

        public abstract OperatorAndValues<string> GetUriOperatorAndValues();

        public abstract OperatorAndValues<TokenValue> GetTokenOperatorAndValues();

        public abstract OperatorAndValues<QuantityValue> GetQuantityOperatorAndValues();

        public abstract OperatorAndValues<ReferenceValue> GetReferenceOperatorAndValues<TResource>( string requestEndPoint ) 
            where TResource : FhirModel.Resource;

        public static Criterium Parse( string key, string value )
        {
            if ( String.IsNullOrEmpty( key ) ) throw new ArgumentNullException( nameof( key ) );
            if ( String.IsNullOrEmpty( value ) ) throw new ArgumentNullException( nameof( value ) );

            // Split chained parts (if any) into name + modifier tuples
            var chainPath = key.Split( new char[] { FhirRest.SearchParams.SEARCH_CHAINSEPARATOR }, StringSplitOptions.RemoveEmptyEntries )
                        .Select( s => PathToKeyAndModifierTuple( s ) );

            if ( chainPath.Count() == 0 ) throw new ArgumentException( "Supplied an empty search parameter name or chain", nameof( key ) );

            return FromPathTuples( chainPath, value );
        }

        public static Criterium CreateFromCompartment( string compartmentName, string compartmentID )
        {
            if ( String.IsNullOrEmpty( compartmentID ) ) throw new ArgumentNullException( nameof( compartmentID ) );

            var paramName = CompartmentSearchParameterNames.FromCompartmentName( compartmentName );
            if ( paramName == null )
            {
                return null;
            }

            return new OperatorCriterium( paramName, null, compartmentID );
        }

        protected string ComputeReferenceCriteriumType<TResource>() where TResource : FhirModel.Resource
        {
            var type = MappingHelper.GetResourceType<TResource>();
            var searchParameter = FhirModel.ModelInfo.SearchParameters
                .FirstOrDefault( p => p.Name == ParamName && p.Resource == type );
            var allowedTypes = searchParameter == null ?
                FhirModel.ModelInfo.SupportedResources.ToArray() :
                searchParameter.Target.Select( rt => rt.ToString() ).ToArray();

            // Workaround for https://github.com/ewoutkramer/fhir-net-api/issues/143
            if ( allowedTypes.Length == 0 && typeof( TResource ) == typeof( FhirModel.MedicationAdministration ) && ParamName == "medication" )
            {
                allowedTypes = new[] { MappingHelper.GetResourceType<FhirModel.Medication>() };
            }
            // Workaround for https://github.com/ewoutkramer/fhir-net-api/issues/143 and/or 
            // http://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemBrowse&feedback=Successfully+added+%5B%239174%5D&tracker_id=677
            if ( allowedTypes.Length == 0 && typeof( TResource ) == typeof( FhirModel.QuestionnaireResponse ) && ParamName == "subject" )
            {
                allowedTypes = new[] { MappingHelper.GetResourceType<FhirModel.Patient>() };
            }

            if ( String.IsNullOrEmpty( _modifier ) )
            {
                if ( allowedTypes.Length > 1 )
                {
                    throw new FhirReferenceWithoutTypeException( this );
                }
                return allowedTypes[ 0 ];
            }

            if ( !allowedTypes.Contains( _modifier ) )
            {
                throw new FhirReferenceTypeNotSupportedException( this, _modifier );
            }

            return _modifier;
        }

        private static Tuple<string, string> PathToKeyAndModifierTuple( string pathPart )
        {
            var pair = pathPart.Split( FhirRest.SearchParams.SEARCH_MODIFIERSEPARATOR );

            string name = pair[ 0 ];
            string modifier = pair.Length == 2 ? pair[ 1 ] : null;

            return Tuple.Create( name, modifier );
        }

        private static Criterium FromPathTuples( IEnumerable<Tuple<string, string>> path, string rawValue )
        {
            var first = path.First();
            var paramName = first.Item1;
            var modifier = first.Item2;

            if ( path.Count() > 1 )
            {
                // Chained search: create a chained criterium, recursively processing the chained parameters
                return new ChainCriterium( paramName, modifier, FromPathTuples( path.Skip( 1 ), rawValue ) );
            }

            return new OperatorCriterium( paramName, modifier, rawValue );
        }

        public static class Prefixes
        {
            public const string Equal = "eq";
            public const string NotEqual = "ne";
            public const string Greater = "gt";
            public const string Less = "lt";
            public const string GreaterOrEqual = "ge";
            public const string LessOrEqual = "le";
            public const string StartsAfter = "sa";
            public const string EndsBefore = "eb";
            public const string ApproximatelyEqual = "ap";
        }

        public static class Modifiers
        {
            public const string Missing = "missing";
            public const string Text = "text";
            public const string Not = "not";
            public const string Above = "above";
            public const string Below = "below";
            public const string In = "in";
            public const string NotIn = "not-in";
            public const string Exact = "exact";
            public const string Contains = "contains";
        }

        protected readonly string _modifier;
    }

    public class ChainCriterium : Criterium
    {
        public ChainCriterium( string paramName, string modifier, Criterium chained ) :
            base( paramName, modifier )
        {
            if ( chained == null ) throw new ArgumentNullException( nameof( chained ) );

            _chained = chained;
        }

        public override string ToString()
        {
            return base.ToString() + FhirRest.SearchParams.SEARCH_CHAINSEPARATOR + _chained.ToString();
        }

        public override Criterium GetChainedCriterium<T, TReference>()
        {
            Criterium result;
            var chainedType = ComputeChainedCriteriumType<T>( out result );

            var referenceType = MappingHelper.GetResourceType<TReference>();
            if ( chainedType != referenceType )
            {
                throw new FhirReferenceTypeNotSupportedException( this, chainedType );
            }

            return result;
        }

        public override OperatorAndValues<decimal> GetNumberOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<DateValue> GetDateOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<string> GetStringOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<string> GetUriOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<TokenValue> GetTokenOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<QuantityValue> GetQuantityOperatorAndValues()
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        public override OperatorAndValues<ReferenceValue> GetReferenceOperatorAndValues<TResource>( string requestEndPoint )
        {
            throw CreateUnexpecterChainedSeachCriteriaException();
        }

        /// <summary>
        /// Computes the resource type that is referenced by a chained search criterium
        /// E.G. if criterium is 'patient.identifier=xxx' with T=Observation, returns Patient - the only resource type that is referenced by Observation.patient
        /// if criterium is 'subject:Patient.identifier=xxx' returns (again) Patient - because it is explicitely specified
        /// if criterium is 'subject.identifier=xxx' throws FhirReferenceWithoutTypeException - because Observation.subject references multiple resource types
        /// </summary>
        /// <typeparam name="T">The type of the resource being searched</typeparam>
        /// <param name="chainedCriterium">Filled with the chained search criterium</param>
        /// <returns>The resource types refrerenced by the chained search criteria.</returns>
        /// <exception cref="FhirReferenceWithoutTypeException">Thrown if there is no modifier and the search parameter references multiple resource types</exception>
        /// <exception cref="FhirReferenceTypeNotSupportedException">Thrown if the type modifier does not match any of the possible referenced types</exception>
        /// <exception cref="FhirUnknownSearchCriteriaException">Thrown if no possible referenced types support the specified chained search criterium</exception>
        private string ComputeChainedCriteriumType<T>(
            out Criterium chainedCriterium
        ) where T : FhirModel.Resource
        {
            var searchType = ComputeReferenceCriteriumType<T>();
            chainedCriterium = _chained;
            var chainedParamName = chainedCriterium.ParamName;
            var result = FhirModel.ModelInfo.SearchParameters
                .FirstOrDefault( s => s.Name == chainedParamName && searchType == s.Resource );
            if ( result == null )
            {
                throw new FhirUnknownSearchCriteriaException( chainedCriterium, searchType );
            }

            return result.Resource;
        }

        private Exception CreateUnexpecterChainedSeachCriteriaException()
        {
            return new FhirInvalidSearchCriteriaException( this, "unexpected chained search parameter" );
        }

        private readonly Criterium _chained;
    }

    public class OperatorCriterium : Criterium
    {
        public OperatorCriterium( string paramName, string modifier, string rawValue ) :
            base( paramName, modifier)
        {
            if ( String.IsNullOrEmpty( rawValue ) ) throw new ArgumentException( nameof( rawValue ) );

            _rawValue = rawValue;
        }

        public override string ToString()
        {
            return base.ToString() + "=" + _rawValue;
        }

        public override Criterium GetChainedCriterium<T, TReference>()
        {
            return null;
        }

        public override OperatorAndValues<decimal> GetNumberOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#number
            return GetOperatorAndValues( 
                GetNumberOperatorAndValue, 
                ( op, value ) => ParseDecimal( value ), 
                Operator.Equal,
                null
            );
        }

        public override OperatorAndValues<DateValue> GetDateOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#date
            return GetOperatorAndValues( 
                GetDateOperatorAndValue,
                ( op, value ) => ParseDate( value ),
                Operator.Equal,
                null
            );
        }

        public override OperatorAndValues<string> GetStringOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#string
            return GetOperatorAndValues( 
                () => Tuple.Create( GetStringOperator(), _rawValue ), 
                ( op, value ) => UnescapeString( value ),
                Operator.StartsWith,
                null
            );
        }

        public override OperatorAndValues<string> GetUriOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#string
            return GetOperatorAndValues(
                () => Tuple.Create( GetUriOperator(), _rawValue ),
                ( op, value ) => UnescapeString( value ),
                Operator.Equal,
                null
            );
        }

        public override OperatorAndValues<TokenValue> GetTokenOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#token
            return GetOperatorAndValues(
                () => Tuple.Create( GetTokenOperator(), _rawValue ),
                ParseToken,
                Operator.Equal,
                Operator.NotEqual
            );
        }

        public override OperatorAndValues<QuantityValue> GetQuantityOperatorAndValues()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#quantity
            return GetOperatorAndValues(
                GetQuantityOperatorAndValue,
                ( op, value ) => ParseQuantity( value ),
                Operator.Equal,
                null
            );
        }

        public override OperatorAndValues<ReferenceValue> GetReferenceOperatorAndValues<TResource>( string requestEndPoint )
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#reference
            return GetOperatorAndValues(
                () => Tuple.Create( Operator.Equal, _rawValue ),
                ( op, value ) => ParseReference<TResource>( value, requestEndPoint ),
                Operator.Equal,
                null
            );
        }

        private OperatorAndValues<T> GetOperatorAndValues<T>( 
            Func<Tuple<Operator, string>> getOperatorAndValue, 
            Func<Operator, string, T> parseValue,
            Operator inOperator,
            Operator? notInOperator
        )
        {
            if ( _modifier == Modifiers.Missing )
            {
                if ( _rawValue == MissingValues.True )
                {
                    return new OperatorAndValues<T>( Operator.IsNull );
                }
                if ( _rawValue == MissingValues.False )
                {
                    return new OperatorAndValues<T>( Operator.IsNotNull );
                }
                throw new FhirInvalidSearchCriteriaException( 
                    this, 
                    "only the values '{0}' and '{1}' are allowed when using the '{2}' modifier", 
                    MissingValues.True, 
                    MissingValues.False, 
                    Modifiers.Missing 
                );
            }

            var operatorAndValue = getOperatorAndValue();
            var op = operatorAndValue.Item1;
            var values = SplitNotEscaped( operatorAndValue.Item2, ',' );

            if ( values.Length == 1 )
            {
                return new OperatorAndValues<T>( op, parseValue( op, operatorAndValue.Item2 ) );
            }

            if ( op == inOperator )
            {
                op = Operator.In;
            }
            else if ( notInOperator.HasValue && op == notInOperator.Value )
            {
                op = Operator.NotIn;
            }
            else
            {
                throw new FhirInvalidSearchCriteriaException( this, "multiple values cannot be used in combination with a comparison operator or modifier" );
            }
            return new OperatorAndValues<T>( op, values.Select( value => parseValue( op, value ) ) );
        }

        private Tuple<Operator, string> GetNumberOperatorAndValue()
        {
            if ( !String.IsNullOrEmpty( _modifier ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "unknown number modifier '{0}'", _modifier );
            }
            return ParseValuePrefix();
        }

        private Tuple<Operator, string> GetDateOperatorAndValue()
        {
            if ( !String.IsNullOrEmpty( _modifier ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "unknown date modifier '{0}'", _modifier );
            }
            return ParseValuePrefix();
        }

        private Tuple<Operator, string> GetQuantityOperatorAndValue()
        {
            if ( !String.IsNullOrEmpty( _modifier ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "unknown quantity modifier '{0}'", _modifier );
            }
            return ParseValuePrefix();
        }

        private Tuple<Operator, string> ParseValuePrefix()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#prefix

            if ( _rawValue.Length >= 2 )
            {
                var op = OperatorFromPrefix( _rawValue.Substring( 0, 2 ) );
                if ( op.HasValue )
                {
                    var value = _rawValue.Substring( 2 );
                    if ( String.IsNullOrEmpty( value ) )
                    {
                        throw new FhirInvalidSearchCriteriaException( this, "the '{0}' comparison operator must be followed by a value", _rawValue.Substring( 0, 2 ) );
                    }
                    return Tuple.Create( op.Value, value );
                }
            }

            return Tuple.Create( Operator.Equal, _rawValue );
        }

        private static Operator? OperatorFromPrefix( string prefix )
        {
            switch ( prefix )
            {
                case Prefixes.Equal:
                    return Operator.Equal;
                case Prefixes.NotEqual:
                    return Operator.NotEqual;
                case Prefixes.Greater:
                    return Operator.Greater;
                case Prefixes.Less:
                    return Operator.Less;
                case Prefixes.GreaterOrEqual:
                    return Operator.GreaterOrEqual;
                case Prefixes.LessOrEqual:
                    return Operator.LessOrEqual;
                case Prefixes.StartsAfter:
                    return Operator.StartsAfter;
                case Prefixes.EndsBefore:
                    return Operator.EndsBefore;
                case Prefixes.ApproximatelyEqual:
                    return Operator.ApproximatelyEqual;
            }
            return null;
        }

        private Operator GetStringOperator()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#string
            if ( String.IsNullOrEmpty( _modifier ) )
            {
                return Operator.StartsWith;
            }

            switch ( _modifier )
            {
                case Modifiers.Exact:
                    return Operator.Equal;
                case Modifiers.Contains:
                    return Operator.Contains;
                case Modifiers.Text:
                    return Operator.Text;
                default:
                    throw new FhirInvalidSearchCriteriaException( this, "unknown string modifier '{0}'", _modifier );
            }
        }

        private Operator GetUriOperator()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#uri
            if ( String.IsNullOrEmpty( _modifier ) )
            {
                return Operator.Equal;
            }

            switch ( _modifier )
            {
                case Modifiers.Above:
                    return Operator.Above;
                case Modifiers.Below:
                    return Operator.Below;
                default:
                    throw new FhirInvalidSearchCriteriaException( this, "unknown URI modifier '{0}'", _modifier );
            }
        }

        private Operator GetTokenOperator()
        {
            // See http://www.hl7.org/implement/standards/fhir/search.html#token
            if ( String.IsNullOrEmpty( _modifier ) )
            {
                return Operator.Equal;
            }

            switch ( _modifier )
            {
                case Modifiers.Text:
                    return Operator.Text;
                case Modifiers.Not:
                    return Operator.NotEqual;
                case Modifiers.Above:
                    return Operator.Above;
                case Modifiers.Below:
                    return Operator.Below;
                case Modifiers.In:
                    return Operator.InValueSet;
                case Modifiers.NotIn:
                    return Operator.NotInValueSet ;
                default:
                    throw new FhirInvalidSearchCriteriaException( this, "unknown token modifier '{0}'", _modifier );
            }
        }

        private decimal ParseDecimal( string value )
        {
            var unescapedValue = UnescapeString( value );
            decimal result;
            if ( !decimal.TryParse( unescapedValue, out result ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid number", unescapedValue );
            }
            return result;
        }

        private DateValue ParseDate( string value )
        {
            var unescapedValue = UnescapeString( value );
            DateTime start, end;
            if ( MappingHelper.TryParseFhirDateTime( unescapedValue, out start, out end ) )
            {
                return new DateValue( start, end );
            }
            throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid date or date-time", unescapedValue );
        }

        private TokenValue ParseToken( Operator op, string value )
        {
            if ( op == Operator.Text || op == Operator.InValueSet || op == Operator.NotInValueSet )
            {
                return new TokenValue( UnescapeString( value ) );
            }

            var pair = SplitNotEscaped( value, '|' );

            if ( pair.Length > 2 )
            {
                throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid token: it cannot have more than two parts separated by '|'", value );
            }

            var pair0 = UnescapeString( pair[ 0 ] );

            var hasNamespace = pair.Length == 2;
            if ( !hasNamespace )
            {
                return new TokenValue( pair0, matchAnyNamespace: true );
            }

            var pair1 = UnescapeString( pair[ 1 ] );
            if ( String.IsNullOrEmpty( pair1 ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid token: it must specify a value after the '|'", value );
            }

            if ( String.IsNullOrEmpty( pair0 ) )
            {
                return new TokenValue( pair1, matchAnyNamespace: false );
            }

            return new TokenValue( pair1, pair0 );
        }

        private QuantityValue ParseQuantity( string value )
        {
            var triple = SplitNotEscaped( value, '|' );

            if ( triple.Length != 3 )
            {
                throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid quantity: it must have three parts separated by '|'", value );
            }

            var number = ParseDecimal( triple[ 0 ] );
            var ns = String.IsNullOrEmpty( triple[ 1 ] ) ? null : UnescapeString( triple[ 1 ] );
            var unit = UnescapeString( triple[ 2 ] );
            if ( String.IsNullOrEmpty( unit ) )
            {
                throw new FhirInvalidSearchCriteriaException( this, "'{0}' is not a valid quantity: it must specify a unit", value );
            }

            return new QuantityValue( number, ns, unit );
        }

        private ReferenceValue ParseReference<TResource>( string value, string requestEndPoint ) where TResource : FhirModel.Resource
        {
            // See http://hl7.org/implement/standards/fhir/search.html#reference
            var unescapedValue = UnescapeString( value );
            string type, id;
            if ( MappingHelper.TryParseResourceLink( new Uri( unescapedValue, UriKind.RelativeOrAbsolute ), requestEndPoint, out type, out id ) )
            {
                // An URL - it specifies type and ID of the object
                if ( !String.IsNullOrEmpty( _modifier ) && _modifier != type )
                {
                    throw new FhirInvalidSearchCriteriaException( this, "unexpected {0} resource type modifier", _modifier );
                }
                return new ReferenceValue( id, type );
            }

            // A direct ID

            return new ReferenceValue( unescapedValue, ComputeReferenceCriteriumType<TResource>() );
        }

        private static string UnescapeString( string value )
        {
            if ( value == null ) return null;

            value = value.Replace( @"\|", @"|" );
            value = value.Replace( @"\,", @"," );
            value = value.Replace( @"\$", @"$" );
            value = value.Replace( @"\\", @"\" );

            return value;
        }

        private static string[] SplitNotEscaped( string value, char separator )
        {
            var word = String.Empty;
            var result = new List<string>();
            bool seenEscape = false;

            for ( int i = 0; i < value.Length; i++ )
            {
                if ( value[ i ] == '\\' )
                {
                    seenEscape = true;
                    continue;
                }

                if ( value[ i ] == separator && !seenEscape )
                {
                    result.Add( word );
                    word = String.Empty;
                    continue;
                }

                if ( seenEscape )
                {
                    word += '\\';
                    seenEscape = false;
                }

                word += value[ i ];
            }

            result.Add( word );

            return result.ToArray<string>();
        }

        private static class MissingValues
        {
            public const string True = "true";
            public const string False = "false";
        }

        private string _rawValue;
    }

    public class OperatorAndValues<T>
    {
        public OperatorAndValues( Operator op ) :
            this( op, null )
        {
            // Empty
        }   

        public OperatorAndValues( Operator op, params T[] values ) :
            this( op, (IEnumerable<T>)values )
        {
            // Empty
        }

        public OperatorAndValues( Operator op, IEnumerable<T> values )
        {
            Operator = op;
            if ( values == null )
            {
                Values = new List<T>();
            }
            else
            {
                Values = new List<T>( values );
            }
        }

        public Operator Operator { get; private set; }

        public IReadOnlyList<T> Values { get; private set; }
    }

    public class DateValue
    {
        public DateValue( DateTime dateTime ) :
            this( dateTime, dateTime )
        {
            // Empty
        }

        public DateValue( DateTime start, DateTime end )
        {
            Start = start;
            End = end;
        }

        public DateTime Start { get; private set; }

        public DateTime End { get; private set; }
    }

    public class TokenValue
    {
        public TokenValue( string value ) :
            this( value, false )
        {
            // Empty
        }

        public TokenValue( string value, bool matchAnyNamespace )
        {
            Value = value;
            AnyNamespace = matchAnyNamespace;
        }

        public TokenValue( string value, string ns )
        {
            Value = value;
            AnyNamespace = false;
            Namespace = ns;
        }

        public string Namespace { get; private set; }

        public string Value { get; private set; }

        public bool AnyNamespace { get; private set; }
    }

    public class QuantityValue
    {
        public QuantityValue( decimal number, string ns, string unit )
        {
            if ( String.IsNullOrEmpty( unit ) ) throw new ArgumentException( nameof( unit ) );

            Number = number;
            Unit = unit;
            Namespace = ns;
        }

        public decimal Number { get; private set; }

        public string Namespace { get; private set; }

        public string Unit { get; private set; }
    }

    public class ReferenceValue
    {
        public ReferenceValue( string id, string type )
        {
            if ( String.IsNullOrEmpty( id ) ) throw new ArgumentException( nameof( id ) );

            ID = id;
            Type = type;
        }

        public string ID { get; private set; }

        public string Type { get; private set; }
    }

    public enum Operator
    {
        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.Less )]
        Less,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.LessOrEqual )]
        LessOrEqual,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.Equal )]
        Equal,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.NotEqual )]
        NotEqual,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.ApproximatelyEqual )]
        ApproximatelyEqual,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.GreaterOrEqual )]
        GreaterOrEqual,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.Greater )]
        Greater,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.StartsAfter )]
        StartsAfter,

        [FhirIntrospection.EnumLiteral( Criterium.Prefixes.EndsBefore )]
        EndsBefore,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.Missing )]
        IsNull,

        [FhirIntrospection.EnumLiteral( "not-missing" )]
        IsNotNull,

        [FhirIntrospection.EnumLiteral( "," )]
        In,

        [FhirIntrospection.EnumLiteral( "not-," )]
        NotIn,

        [FhirIntrospection.EnumLiteral( "starts-with" )]
        StartsWith,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.Contains )]
        Contains,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.Text )]
        Text,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.Above )]
        Above,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.Below )]
        Below,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.In )]
        InValueSet,

        [FhirIntrospection.EnumLiteral( Criterium.Modifiers.NotIn )]
        NotInValueSet,
    }

@cknaap
Copy link
Member

cknaap commented Jan 11, 2016

Indeed it does happen in Criterium.cs and it happens irrespectively of the type of search parameter. Michele, thanks for your suggestions. Maybe I missed it, but I still don't see where you decide which type of parameter / value you're parsing, hence whether you need to be aware of operator prefixes or not.

The structural solution would be to defer the parsing of the value to a point where we know the type of searchparameter. If you look at MongoSearcher.Search, this means splitting the parsing in 2 parts:

  1. The current parseCriteria: determine the resourcetype and parametername for each parameter.
  2. EnrichCriteriaWithSearchParameters -> after that we know which searchparameter belongs to it, and hence the type of argument.
  3. The second part of parsing: parse the argument, stripping off operator prefixes only if allowed on this type of searchparameter.

I just won't be able to do this very shortly...

@michelemottini
Copy link
Author

In the code I posted the GetXXXXOperatorAndValues() method does the parsing for a XXXX type parameter.

The search code calls the appropriate GetXXXXOperatorAndValues() method depending on the type of the parameter it is parsing

@mharthoorn mharthoorn modified the milestone: Spark 12 - Montreal Aug 29, 2016
@mharthoorn mharthoorn added this to the Spark 13 - Baltimore milestone Aug 30, 2016
@kennethmyhra kennethmyhra removed this from the Spark - DevDays 2016 milestone Nov 12, 2019
@kennethmyhra kennethmyhra added this to To do in R2020.1 via automation Nov 12, 2019
@kennethmyhra kennethmyhra added this to To do in R2020.2 via automation Feb 16, 2020
@kennethmyhra kennethmyhra removed this from To do in R2020.1 Feb 16, 2020
@kennethmyhra kennethmyhra removed this from To do in R2020.2 Aug 11, 2020
@kennethmyhra kennethmyhra added this to To do in R2020.3 via automation Aug 11, 2020
@kennethmyhra kennethmyhra moved this from To do to Done in R2020.3 Sep 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
R2020.3
  
Done
Development

No branches or pull requests

7 participants