1:n relationships and complex attribute types in ALFA - xacml

I'm trying to enter our database model into ALFA in order to check the capabilities of ALFA and XACML.
Are attributes like the following possible? How would look the rules then?
1:n by list of strings
namespace com.mycompany {
namespace resources {
namespace patient {
attribute trustedDoctorIds{
category = resourceCat
id = "trustedDoctorIds"
type = list<string> //maybe it should be bag[string]
}
}
}
}
1:n by list of complex type
namespace com.mycompany {
namespace resources {
namespace patient {
attribute trustedDoctors{
category = resourceCat
id = "trustedDoctors"
type = list<doctor> //maybe it should be bag[doctor]
}
}
}
namespace subjects {
namespace doctor {
attribute id {
category = subjectCat
id = "id"
type = string
}
attribute lastname {
category = subjectCat
id = "lastname"
type = string
}
}
}
}

You have a great question there.
By default all attributes in ALFA and XACML are multi-valued. Attributes are bags of values rather than single values. This means that when you define the following,
attribute trustedDoctorIds{
category = resourceCat
id = "trustedDoctorIds"
type = string
}
This means the attribute has a type of string and it can be multi-valued. You could choose to express cardinality information in the comments above the attribute definition e.g.
/**
* This attribute, trustedDoctorIds, contains the list of doctors a patient
*trusts. The list can have 0 or more values.
*/
The policy is the one that will convey how many values there can be depending on the functiosn being used.
For instance, you could write a condition that states
stringOneAndOnly(trustedDoctorIds)==stringOneAndOnly(userId)
In that case, you are forcing each attribute to have one value and one value only. If you have 0 or more than 1 value, then the evaluation of the XACML policy will yield Indeterminate.
In a XACML (or ALFA) target, when you write:
trustedDoctorIds == "Joe"
You are saying: if there is at least one value in trustedDoctorIds equal to 'Joe'...
In an ALFA condition, when you write
trustedDoctorIds==userId
You are saying: *if there is at least one value in trustedDoctorIds equal to at least one value in userId
Note: I always use singular names for my attributes when I can. It's a convention, not a hard limit. Remembering the cardinality of your attributes will help later in your policy testing.
Answers to the comments
What would be a plural name you try to avoid by your convention?
Well trustedDoctorId***s*** looks rather plural to me. I would use trustedDoctorId unless you know that the attribute is necessarily always multi-valued.
So, this should be possible: In my request I provide resource.patient.trustedDoctorIds=="2,13,67" and subject.doctor.id=="6". How would the rule then look like in ALFA? Smth. like "resource.patient.trustedDoctorIds.contains(subject.doctor.id) permit"
The rule would look like the following:
stringIsIn(stringOneAndOnly(subject.doctor.id),resource.patient.trustedDoctorIds)
Make sure that you provide multiple values in your request, not one value that contains comma-separated values. Send in [1,2,3] rather than "1,2,3".
Further edits
So, by [2,13,67] the result is deny as expected and not permit like with "2,13,67" and doctorId==6. I chose that example on purpose, since the stringIsIn function would result unwantedly with true since 6 is included in 67
Do not confuse stringIsIn() and stringContains().
stringIsIn(a, b) takes in 2 parameters a and b where a is an atomic value and b is a bag of values. stringIsIn(a, b) returns true if the value of a is in the bag of values of b.
stringContains(a, b) takes in 2 parameters a and b that are both atomic values of type string. It returns true if the string value a is found inside b.
Example:
stringIsIn(stringOneAndOnly(userCitizenship), stringBag("Swedish", "German")) returns true if the user has a single citizenship equal to either of Swedish or German.
stringContains("a", "alfa") returns true if the second string contains the first one. So it returns true in this example.

Related

What is the best practice of iterating record keys and values in Reasonml?

I'm new to ReasonML, but I read through most of the official documents. I could go through the casual trial and errors for this, but since I need to write codes in ReasonML right now, I'd like to know the best practices of iterating keys and values of reason record types.
I fully agree with #Shawn that you should use a more appropriate data structure. A list of tuples, for example, is a nice and easy way to pass in a user-defined set of homogeneous key/value pairs:
fooOnThis([
("test1", ["a", "b", "c"]),
("test2", ["c"]),
])
If you need heterogeneous data I would suggest using a variant to specify the data type:
type data =
| String(string)
| KvPairs(list((string, data)));
fooOnThis([
("test1", [String("a"), String("b"), String("c")]),
("test2", [String("c"), KvPairs([("innerTest", "d")])]),
])
Alternatively you can use objects instead of records, which seems like what you actually want.
For the record, a record requires a pre-defined record type:
type record = {
foo: int,
bar: string,
};
and this is how you construct them:
let value = {
foo: 42,
bar: "baz",
};
Objects on the other hand are structurally typed, meaning they don't require a pre-defined type, and you construct them slightly differently:
let value
: {. "foo": int, "bar": string }
= {"foo": 42, "bar": "baz"};
Notice that the keys are strings.
With objects you can use Js.Obj.keys to get the keys:
let keys = Js.Obj.keys(value); // returns [|"foo", "bar"|]
The problem now is getting the values. There is no Js.Obj API for getting the values or entries because it would either be unsound or very impractical. To demonstrate that, let's try making it ourselves.
We can easily write our own binding to Object.entries:
[#bs.val] external entries: Js.t({..}) => array((string, _)) = "Object.entries";
entries here is a function that takes any object and returns an array of tuples with string keys and values of a type that will be inferred based on how we use them. This is neither safe, because we don't know what the actual value types are, or particularly practical as it will be homogeneously typed. For example:
let fields = entries({"foo": 42, "bar": "baz"});
// This will infer the value's type as an `int`
switch (fields) {
| [|("foo", value), _|] => value + 2
| _ => 0
};
// This will infer the value's type as an `string`, and yield a type error
// because `fields` can't be typed to hold both `int`s and `string`s
switch (fields) {
| [|("foo", value), _|] => value ++ "2"
| _ => ""
};
You can use either of these switch expressions (with unexpected results and possible crashes at runtime), but not both together as there is no unboxed string | int type to be inferred in Reason.
To get around this we can make the value an abstract type and use Js.Types.classify to safely get the actual underlying data type, akin to using typeof in JavaScript:
type value;
[#bs.val] external entries: Js.t({..}) => array((string, value)) = "Object.entries";
let fields = entries({"foo": 42, "bar": "baz"});
switch (fields) {
| [|("foo", value), _|] =>
switch (Js.Types.classify(value)) {
| JSString(str) => str
| JSNumber(number) => Js.Float.toString(number)
| _ => "unknown"
}
| _ => "unknown"
};
This is completely safe but, as you can see, not very practical.
Finally, we can actually modify this slightly to use it safely with records as well, by relying on the fact that records are represented internally as JavaScript objects. All we need to do is not restrict entries to objects:
[#bs.val] external entries: 'a => array((string, value)) = "Object.entries";
let fields = keys({foo: 42, bar: 24}); // returns [|("foo", 42), ("bar", 24)|]
This is still safe because all values are objects in JavaScript and we don't make any assumptions about the type of the values. If we try to use this with a primitive type we'll just get an empty array, and if we try to use it with an array we'll get the indexes as keys.
But because records need to be pre-defined this isn't going to be very useful. So all this said, I still suggest going with the list of tuples.
Note: This uses ReasonML syntax since that's what you asked for, but refers to the ReScript documentation, which uses the slightly different ReScript syntax, since the BuckleScript documentation has been taken down (Yeah it's a mess right now, I know. Hopefully it'll improve eventually.)
Maybe I am not understanding the question or the use case. But as far as I know there is no way to iterate over key/value pairs of a record. You may want to use a different data model:
hash table https://caml.inria.fr/pub/docs/manual-ocaml/libref/Hashtbl.html
Js.Dict (if you're working in bucklescript/ReScript) https://rescript-lang.org/docs/manual/latest/api/js/dict
a list of tuples
With a record all keys and value types are known so you can just write code to handle each one, no iteration needed.

Secondary Index on Custom Java Object in Aerospike

I have two classes.
class A {
String aName;
B b;
public A(String aName, B b) {
this.aName = aName;
this.b = b;
}
public String getaName() {
return aName;
}
public B getB() {
return b;
}
}
class B {
String bName;
public B(String bName) {
this.bName = bName;
}
public String getbName() {
return bName;
}
}
I am storing A as a set in Aerospike and A.aName is primary key. I want a secondary key on A.b. I have created index on A.b attribute and able to persist also. But search from the index is not returning anything. As per my understanding, Aerospike supports only three type of indexes: String, Numeric and Geo,. Is there any option for custom object.
Actually you can also index string, numeric and geo within different source types - basic (meaning just a bin with scalar data), list (so you can index strings or numeric data that is contained in a list), map keys and map values.
See: https://www.aerospike.com/docs/guide/query.html#secondary-index-key-source-type
You could model this in a few ways:
As a map. Let's assume that you store the value of A in a bin whose type is a map, it could have two map-keys - aName and bName. You can build a secondary index for string data on map values. Now, if you search for a specific bName you'll have this record come up.
More rationally you'd do the following as separate bins.
Assume that you use several bins, among them two bins to hold aname and bname. Their values are strings. You could now build a secondary index for string values and a basic index type (string data not contained in a complex data type such as list or map). You'd query for all the records where the predicate is bname = foo.
For a more complex situation one or more bname values map to a single aname, you'd actually model this as a lookup table.
Assume a set (table) called users holding records whose keys are the aname. A single key-value operation such as a read or an upsert works on a specific instance of class A, identified by a given aname. One record in Aerospike per instance of A.
You have another set (table) as the lookup table, where for each unique bname you create a record. The specific bname is the key to this record. The value is the aname. So to find the record in the users set, you first look it up in the lookup table by the bname. You use the value of this record as the key for the users record for that aname. This is the most common way of modeling this type of use case, without a secondary index.
Here is the answer post on Aerospike forum.
Binary blobs can be anything, there’s definitely no way to index that. If there is a particular field in your class you can pull out and set as a separate bin with a string/long type then that would work
https://discuss.aerospike.com/t/secondary-index-on-custom-java-object/6485

Targeting the first and second values in a Class<Int, Int>

In Kotlin's Pair data class it takes 2 values:
Pair<out A, out B>
and those values are targeted via first and second named properties:
Pair(first: A, second: B)
However, how are the first and properties targeted if these properties didn't exist (e.g. for other classes with two parameters OtherClass<Int, Int>? - Is there another way to target them?)
PS: Why is Pair a data class and not a regular class?
A and B are not properties, they are type parameters. The type parameters are then used to define properties.
This is the source code for Pair:
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable {
/**
* Returns string representation of the [Pair] including its [first] and [second] values.
*/
public override fun toString(): String = "($first, $second)"
}
You target the properties (first and second), not the type parameters.
Suppose for example you have Pair<Int, String>. This means A is Int, B is String.
first is a property of type A, second is a property of type B, therefore first is an Int and second is a String.
In answer to your 'PS', Pair is a data class because of the usual benefits this confers, e.g. a generated equals() method.
Pair repersents a generic pair of two values, think of it as a box with a divider in the middle. It doesn't care what you put in each side of the divider.
What are First and Second:
If you look at Robs Answer you'll see that Pair class has two public values, one is
name "first" and the other is named "second". It basically means that every box(Pair)
has two sides and it does not care what you put in either side, by default I have named
one side as "first" and the other side as "second" if you want something from left just
refer to it as first and if you want something from right side refer to it as second.
Doing Pair("Some Words", 99) simply means you want a box that one side holds a string and the other side holds an integer.
Doing myPair = Pair("value 1", 100) simply means you want a box that one side holds a string(value 1) and the other side holds an integer(100) and you have named this box myPair.
Since Kotlin does not care about the type of things that your putting in the box, than it is perfectly okay to add a null value to a pair, like myPair=Pair(null, 99), its your job to check if there is a null value being added or extracted from the pair.
Retrieving values:
Say you do something like var myPair = Pair("value 1", 100)
If you want to see what you are holding in myPair you can do:
println(myPair.first) -> "value 1"
println(myPair.second) -> 100
Why is Pair a data class and not a regular class?
The main purpose of a data class is to hold data.
Data classes have some restrictions so the compiler can add some standard functionality to all data classes, like eaquls(), hashCode()
Pair class is meant to hold a pair of data, therefore in can benefit from some standard functionality, like toString().
You can read the details HERE
If you take a look at how Pair is implemented, you see that it's a simple data class with two properties: first and second.
public data class Pair<out A, out B>(
public val first: A,
public val second: B
)
As a result, you can obviously access these properties with this syntax: pair.first and pair.second. The names first and second are no special keywords but simply property names.
Note that there's something special about pairs and data classes in general though, which can be observed here:
val p = Pair(1, 2)
val (f: Int, s: Int) = p
println(f) //1
println(s) //2
This technique is called destructuring and is made possible through componentX operators which are automatically generated for you if you use data classes.

Optional parameters in Fsharp records

I need to modify and add new property for my F Sharp record. but then it gives errors for the previous instances which are made without this new field. I made it as Nullable ,but still the same error occures, Please help me to solve this
I presume you mean "optional" as in "a field that I don't provide when instantiating the record". However, there is no such thing as an optional field in F# records unfortunately (or fortunately, depending on your point of view). All record fields that you specify must be present at the point when you instantiate it.
See also this closely related question.
You may consider this trade-off:
Use a record. Each time you update the record, the F# compiler will scream and alert you about all places where you used this record, and you need to now provide the additional information. The additional information can be a None for the fields you added, if they are options. The big advantage: Your code itself will not have to handle all of the possibly many cases of "what to do if field1 is missing? What if field2 is missing, too?"
Use a class. When you update the class, the F# compiler will not enforce any sort of completeness check about the information you put into the class. (You can view records as classes where all fields are constructor arguments, and all must be provided). Hence, updating the class definition causes no overhead, but your code needs handle all the missing values.
I personally prefer records just because it forces me to think through the implications of adding a new field.
There is of course a middle ground: You can use records, but instantiate all of them via static members or something alike:
type Name =
{
First: string
Family: string
}
static member Create(first, family) = { First = first; Family = family}
If, in your code, you always use Name.Create to instantiate the record, you will of course be able to add a MiddleName field without any consumer code noticing.
The Option type is preferred over null in F#, for the simple reason that "uninitialized variables" do not exist in the functional way of thinking.
Let's start by building a record that represents a VacationRequest, which has to be approved by the boss.
type VacationRequest =
{Name : string
Date : DateTime
Approval : string option}
The problem with your approach is that all fields have to be assigned on construction, so this won't compile:
let holiday =
{Name = "Funk"
Date = DateTime(2020,12,31)}
You can work around this using a helper function, which implicitly sets the option value.
let fillInRequest name date =
{Name = name
Date = date
Approval = None}
Now you can build the record using the helper function.
let holiday = fillInRequest "Funk" <| DateTime(2020,12,31)
Did notice something funny when sending the code to FSI.
val holiday : VacationRequest = {Name = "Funk";
Date = 31/12/2020 12:00:00 ;
Approval = null;}
The boss could then update the request (creating a new record)
let approvedHoliday =
{holiday with Approval = Some "boss' name"}
val approvedHoliday : VacationRequest = {Name = "Funk";
Date = 31/12/2020 12:00:00 ;
Approval = Some "boss' name";}
or send it back unaltered
let betterLuckNextTime = holiday

How to design generic filtering operators in the query string of an API?

I'm building a generic API with content and a schema that can be user-defined. I want to add filtering logic to API responses, so that users can query for specific objects they've stored in the API. For example, if a user is storing event objects, they could do things like filter on:
Array contains: Whether properties.categories contains Engineering
Greater than: Whether properties.created_at is older than 2016-10-02
Not equal: Whether properties.address.city is not Washington
Equal: Whether properties.name is Meetup
etc.
I'm trying to design filtering into the query string of API responses, and coming up with a few options, but I'm not sure which syntax for it is best...
1. Operator as Nested Key
/events?properties.name=Harry&properties.address.city.neq=Washington
This example is uses just a nested object to specific the operators (like neq as shown). This is nice in that it is very simple, and easy to read.
But in cases where the properties of an event can be defined by the user, it runs into an issue where there is a potential clash between a property named address.city.neq using a normal equal operator, and a property named address.city using a not equal operator.
Example: Stripe's API
2. Operator as Key Suffix
/events?properties.name=Harry&properties.address.city+neq=Washington
This example is similar to the first one, except it uses a + delimiter (which is equivalent to a space) for operations, instead of . so that there is no confusion, since keys in my domain can't contain spaces.
One downside is that it is slightly harder to read, although that's arguable since it might be construed as more clear. Another might be that it is slightly harder to parse, but not that much.
3. Operator as Value Prefix
/events?properties.name=Harry&properties.address.city=neq:Washington
This example is very similar to the previous one, except that it moves the operator syntax into the value of the parameter instead of the key. This has the benefit of eliminating a bit of the complexity in parsing the query string.
But this comes at the cost of no longer being able to differentiate between an equal operator checking for the literal string neq:Washington and a not equal operator checking for the string Washington.
Example: Sparkpay's API
4. Custom Filter Parameter
/events?filter=properties.name==Harry;properties.address.city!=Washington
This example uses a single top-level query paramter, filter, to namespace all of the filtering logic under. This is nice in that you never have to worry about the top-level namespace colliding. (Although in my case, everything custom is nested under properties. so this isn't an issue in the first place.)
But this comes at a cost of having a harder query string to type out when you want to do basic equality filtering, which will probably result in having to check the documentation most of the time. And relying on symbols for the operators might lead to confusion for non-obvious operations like "near" or "within" or "contains".
Example: Google Analytics's API
5. Custom Verbose Filter Parameter
/events?filter=properties.name eq Harry; properties.address.city neq Washington
This example uses a similar top-level filter parameter as the previous one, but it spells out the operators with word instead of defining them with symbols, and has spaces between them. This might be slightly more readable.
But this comes at a cost of having a longer URL, and a lot of spaces that will need to be encoded?
Example: OData's API
6. Object Filter Parameter
/events?filter[1][key]=properties.name&filter[1][eq]=Harry&filter[2][key]=properties.address.city&filter[2][neq]=Washington
This example also uses a top-level filter parameter, but instead of creating a completely custom syntax for it that mimics programming, it instead builds up an object definition of filters using a more standard query string syntax. This has the benefit of bring slightly more "standard".
But it comes at the cost of being very verbose to type and hard to parse.
Example Magento's API
Given all of those examples, or a different approach, which syntax is best? Ideally it would be easy to construct the query parameter, so that playing around in the URL bar is doable, but also not pose problems for future interoperability.
I'm leaning towards #2 since it seems like it is legible, but also doesn't have some of the downsides of other schemes.
I might not answer the "which one is best" question, but I can at least give you some insights and other examples to consider.
First, you are talking about "generic API with content and a schema that can be user-defined".
That sound a lot like solr / elasticsearch which are both hi level wrappers over Apache Lucene which basically indexes and aggregates documents.
Those two took totally different approaches to their rest API, I happened to work with both of them.
Elasticsearch :
They made entire JSON based Query DSL, which currently looks like this :
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
Taken from their current doc. I was surprised that you can actually put data in GET...
It actually looks better now, in earlier versions it was much more hierarchical.
From my personal experience, this DSL was powerful, but rather hard to learn and use fluently (especially older versions). And to actually get some result you need more than just play with URL. Starting with the fact that many clients don't even support data in GET request.
SOLR :
They put everything into query params, which basically looks like this (taken from the doc) :
q=*:*&fq={!cache=false cost=5}inStock:true&fq={!frange l=1 u=4 cache=false cost=50}sqrt(popularity)
Working with that was more straightforward. But that's just my personal taste.
Now about my experiences. We were implementing another layer above those two and we took approach number #4. Actually, I think #4 and #5 should be supported at the same time. Why? Because whatever you pick people will be complaining, and since you will be having your own "micro-DSL" anyway, you might as well support few more aliases for your keywords.
Why not #2? Having single filter param and query inside gives you total control over DSL. Half a year after we made our resource, we got "simple" feature request - logical OR and parenthesis (). Query parameters are basically a list of AND operations and logical OR like city=London OR age>25 don't really fit there. On the other hand parenthesis introduced nesting into DSL structure, which would also be a problem in flat query string structure.
Well, those were the problems we stumbled upon, your case might be different. But it is still worth to consider, what future expectations from this API will be.
Matomo Analytics has an other approach to deal with segment filter and its syntaxe seems to be more readable and intuitive, e.g:
developer.matomo.org/api-reference/reporting-api-segmentation
Operator
Behavior
Example
==
Equals
&segment=countryCode==IN Return results where the country is India
!=
Not equals
&segment=actions!=1 Return results where the number of actions (page views, downloads, etc.) is not 1
<=
Less than or equal to
&segment=actions<=4 Return results where the number of actions (page views, downloads, etc.) is 4 or less
<
Less than
&segment=visitServerHour<12 Return results where the Server time (hour) is before midday.
=#
Contains
&segment=referrerName=#piwik Return results where the Referer name (website domain or search engine name) contains the word "piwik".
!#
Does not contain
&segment=referrerKeyword!#yourBrand Return results where the keyword used to access the website does not contain word "yourBrand".
=^
Starts with
&segment=referrerKeyword=^yourBrand Return results where the keyword used to access the website starts with "yourBrand" (requires at least Matomo 2.15.1).
=$
Ends with
&segment=referrerKeyword=$yourBrand Return results where the keyword used to access the website ends with "yourBrand" (requires at least Matomo 2.15.1).
and you can have a close look at how they parse the segment filter here: https://github.com/matomo-org/matomo/blob/4.x-dev/core/Segment/SegmentExpression.php
#4
I like how Google Analytics filter API looks like, easy to use and easy to understand from a client's point of view.
They use a URL encoded form, for example:
Equals: %3D%3D filters=ga:timeOnPage%3D%3D10
Not equals: !%3D filters=ga:timeOnPage!%3D10
Although you need to check documentation but it still has its own advantages. IF you think that the users can get accustomed to this then go for it.
#2
Using operators as key suffixes also seems like a good idea (according to your requirements).
However I would recommend to encode the + sign so that it isn't parsed as a space. Also it might be slightly harder to parse as mentioned but I think you can write a custom parser for this one. I stumbled across this gist by jlong some time back. Perhaps you'll find it useful to write your parser.
You could also try Spring Expression Language (SpEL)
All you need to do is to stick to the said format in the document, the SpEL engine would take care of parsing the query and executing it on a given object. Similar to your requirement of filtering a list of objects, you could write the query as:
properties.address.city == 'Washington' and properties.name == 'Harry'
It supports all kind of relational and logical operators that you would need. The rest api could just take this query as the filter string and pass it to SpEL engine to run on an object.
Benefits: it's readable, easy to write, and execution is well taken care of.
So, the URL would look like:
/events?filter="properties.address.city == 'Washington' and properties.name == 'Harry'"
Sample code using org.springframework:spring-core:4.3.4.RELEASE :
The main function of interest:
/**
* Filter the list of objects based on the given query
*
* #param query
* #param objects
* #return
*/
private static <T> List<T> filter(String query, List<T> objects) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(query);
return objects.stream().filter(obj -> {
return exp.getValue(obj, Boolean.class);
}).collect(Collectors.toList());
}
Complete example with helper classes and other non-interesting code:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpELTest {
public static void main(String[] args) {
String query = "address.city == 'Washington' and name == 'Harry'";
Event event1 = new Event(new Address("Washington"), "Harry");
Event event2 = new Event(new Address("XYZ"), "Harry");
List<Event> events = Arrays.asList(event1, event2);
List<Event> filteredEvents = filter(query, events);
System.out.println(filteredEvents.size()); // 1
}
/**
* Filter the list of objects based on the query
*
* #param query
* #param objects
* #return
*/
private static <T> List<T> filter(String query, List<T> objects) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(query);
return objects.stream().filter(obj -> {
return exp.getValue(obj, Boolean.class);
}).collect(Collectors.toList());
}
public static class Event {
private Address address;
private String name;
public Event(Address address, String name) {
this.address = address;
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Address {
private String city;
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
}
I decided to compare the approaches #1/#2 (1) and #3 (2) and concluded that (1) is preferred (at least, for Java server side).
Assume, some parameter a must be equal 10 or 20. Our URL query in this case must look like ?a.eq=10&a.eq=20 for (1) and ?a=eq:10&a=eq:20 for (2). In Java HttpServletRequest#getParameterMap() will return the next values: { a.eq: [10, 20] } for (1) and { a: [eq:10, eq:20] } for (2). Later we must convert returned maps, for example, to SQL where clause. And we should get: where a = 10 or a = 20 for both (1) and (2). Briefly, it looks something like that:
1) ?a=eq:10&a=eq:20 -> { a: [eq:10, eq:20] } -> where a = 10 or a = 20
2) ?a.eq=10&a.eq=20 -> { a.eq: [10, 20] } -> where a = 10 or a = 20
So, we got the next rule: when we pass through URL query two parameters with the same name we must use OR operand in SQL.
But let's assume another case. The parameter a must be greater than 10 and less than
20. Applying the rule above we will have the next conversion:
1) ?a.gt=10&a.ls=20 -> { a.gt: 10, a.lt: 20 } -> where a > 10 and a < 20
2) ?a=gt:10&a=ls:20 -> { a: [gt.10, lt.20] } -> where a > 10 or(?!) a < 20
As you can see, in (1) we have two parameters with different names: a.gt and a.ls. This means our SQL query will have AND operand. But for (2) we still have the same names and it must be converted to the SQL with OR operand!
This means that for (2) instead of using #getParameterMap() we must directly parse the URL query and analyze repeated parameter names.
I know this is old school, but how about a sort of operator overloading?
It would make the query parsing a lot harder (and not standard CGI), but would resemble the contents of an SQL WHERE clause.
/events?properties.name=Harry&properties.address.city+neq=Washington
would become
/events?properties.name=='Harry'&&properties.address.city!='Washington'||properties.name=='Jack'&&properties.address.city!=('Paris','New Orleans')
paranthesis would start a list. Keeping strings in quotes would simplify parsing.
So the above query would be for events for Harry's not in Washington or for Jacks not in Paris or in New Orleans.
It would be a ton of work to implement... and the database optimization to run those queries would be a nightmare, but if you're looking for a simple and powerful query language, just imitate SQL :)
-k