For me:
neo4jSession.query("MATCH (n:Widget) WHERE (n.partNumber STARTS WITH '001') RETURN n.partNumber AS id, n.name AS description, n.urn AS urn LIMIT 10", Collections.emptyMap());
works.
This query does not work:
String query = "MATCH (n:Widget) " +
"WHERE (n.partNumber STARTS WITH {queryString}) " +
"RETURN n.partNumber AS id, n.name AS description, n.urn AS urn " +
"LIMIT {limit}";
Map<String, Object> params = ImmutableMap
.<String, Object>builder()
.put("queryString", queryString)
.put("limit", limit)
.build();
return (List) neo4jOperations.queryForObjects(Object.class, query, params);
It returns an empty list. I've also tried with my actual domain object:
return (List) neo4jOperations.queryForObjects(Widget.class, query, params);
with the same result.
I'm using OGM 2.0.2, neo4j 2.3.2, and Spring Data Neo4j 4.1.1 BUT I've tried this without neo4jOperations using Neo4jSession by itself with the same results. Oh, also I'm using a remove instance of neo4j with the HTTP driver.
Is there a bug in OGM?
MORE INFO:
Over the wire, I BELIEVE the messages look like this:
{ "statements":[
{
"statement":"MATCH (n:Widget) WHERE (n.partNumber STARTS WITH {queryString}) RETURN n.partNumber AS id, n.name AS description, n.urn AS urn LIMIT {limit}",
"parameters":{
"queryString":"001",
"limit":10
},
"resultDataContents":[
"graph"
],
"includeStats":false
} ] }
{ "statements":[
{
"statement":"MATCH (n:Widget) WHERE (n.partNumber STARTS WITH '001') RETURN n.partNumber AS id, n.name AS description, n.urn AS urn LIMIT 10",
"parameters":{
},
"resultDataContents":[
"rest"
],
"includeStats":true
} ] }
EVEN MORE INFORMATION:
I've tried this with Widget as both #QueryResult AND as #NodeEntity (w/ getters and setters).
#QueryResult
public class TypeaheadData {
public Object id;
public String description;
public String uid;
}
AND
#NodeEntity
public class TypeaheadData {
public Object id;
public String description;
public String uid;
public TypeaheadData() {
}
public Object getId() {
return id;
}
public void setId(Object id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
}
I've ALSO inspected the response over the wire and in both cases it looks like this:
{
"results":[
{
"columns":[
"id",
"description",
"uid"
],
"data":[
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
},
{
"graph":{
"nodes":[
],
"relationships":[
]
}
}
]
}
],
"errors":[
]
}
IF i remove the Widget #NodeEntity, this is the request sent out:
{
"statements":[
{
"statement":"MATCH (n:Widget) WHERE (n.partNumber STARTS WITH {queryString}) RETURN n.partNumber AS id, n.name AS description, n.urn AS urn LIMIT {limit}",
"parameters":{
"queryString":"001",
"limit":10
},
"resultDataContents":[
"row"
],
"includeStats":false
}
]
}
AND having removed the Widget #NodeEntity, the response DOES have the correct data in it, but the mapper throws:
Scalar response queries must only return one column. Make sure your
cypher query only returns one item.
The OGM cannot map a collection of properties into a domain entity.
Your query returns:
RETURN n.partNumber AS id, n.name AS description, n.urn AS urn
but there is nothing to tell the OGM what kind of entity this is, if it is one at all.
Changing this to RETURN n should do the job with neo4jOperations.queryForObjects(Widget.class, query, params);
Neo4j OGM can't handle mapping queries that don't return entire node objects. If you request only a subset of a node's properties in a query, you have to use the query method that returns a Result. And then you have to do the mapping yourself.
If you're using spring-data-neo4j, then you can use their #QueryResult annotation mixed with a repository #Query to handle the mapping for you. If you take a look at the code, they've finagled a mapper from from the metadata provided by the Neo4jSession.
The one exception is if you are querying for single properties on nodes, then the queryForObjects function will work.
Seem's like an oversight to me, but who am I to say.
Related
Here is my Dto for percolator query class.
#Data
#Document(indexName = "#{#es.indexName}")
#Builder(builderClassName = "RuleBuilder")
public class Rule {
#Id
private String id = UUID.randomUUID().toString();
private QueryBuilder query;
private RuleDataDto data;
public static class RuleBuilder {
private String id = UUID.randomUUID().toString();
}
}
Index Mapping
{
"mappings": {
"properties": {
"query": {
"type": "percolator"
},
"data": {
"properties": {
"subType": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"type": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"content": {
"type": "text"
}
}
}
}
based on criteria I am generating queries and trying to index those to percolator. but getting below exception
query malformed, no start_object after query name
what should be the query field Type? can someone help me on this
You are trying to store an Elasticsearch object (QueryBuilder) in Elasticsearch without specifying the mapping type.
You will need to annotate your query property as percolator type and might change the type of your property to a String:
#Document(indexName = "#{#es.indexName}")
public class Rule {
#Id
private String _id;
#Field(type = FieldType.Percolator)
private String query;
// ...
}
Or, if you want to have some other class for the query property you'll need a custom converter that will convert your object into a valid JSON query - see the documentation for the percolator mapping.
Im trying to parse the the following json and map it to equivalent objects:
[
{
"name":"Will Everett",
"golfers":[
{
"name":"Bryson DeChambeau",
"rounds":[
{
"number":1,
"score":"75"
}
],
"today":"-",
"thru":"7:41 PM"
},
{
"name":"Shane Lowry",
"rounds":[
{
"number":1,
"score":"75"
},
{
"number":2,
"score":"--"
}
],
"today":"-",
"thru":"7:41 PM"
},
{
"name":"Gary Woodland",
"rounds":[
{
"number":1,
"score":"75"
},
{
"number":2,
"score":"--"
}
],
"today":"-",
"thru":"7:41 PM"
},
{
"name":"Danny Willett",
"rounds":[
{
"number":1,
"score":"75"
},
{
"number":2,
"score":"--"
}
],
"today":"-",
"thru":"7:41 PM"
}
]
}
]
My models to parse the code starts from the user class and populates from within:
class User {
String name;
List<Golfer> golfers;
User({#required this.name,
#required this.golfers});
static User fromJson(Map<String, dynamic> json) {
return User(name: json["name"],
golfers: json["golfers"].map((golfer) => Golfer.fromJson(golfer)).toList());
}
}
class Golfer {
String name;
List<Round> rounds;
String today;
String thru;
String score(int roundNumber) {
return rounds[roundNumber].score;
}
Golfer({#required this.name,
#required this.rounds,
#required this.today,
#required this.thru});
static Golfer fromJson(Map<String, dynamic> json) {
return Golfer(name: json["name"],
rounds: json["rounds"].map((round) => Round.fromJson(round)).toList(),
today: json["today"].toString(),
thru: json["thru"].toString());
}
}
class Round {
String number;
String score;
Round({#required this.number,
#required this.score});
static Round fromJson(Map<String, dynamic> json) {
return Round(number: json["number"].toString(),
score: json["score"].toString());
}
}
But when i call json.decode(response.body)).map((user) => User.fromJson(user)).toList() i get the following error expected a value of type 'List<Round>', but got one of type 'List<dynamic>'
i cant see where i am going wrong and im sure its simple but i just cant see it.
The error is so specific too but i cant seem to parse the rounds properly for some reason.
The link to the json is here
try defining generic types when converting toList()
e.g.
replace
rounds: json["rounds"].map((round) => Round.fromJson(round)).toList(),
with
rounds: json["rounds"].map<Round>((round) => Round.fromJson(round)).toList(),
and
golfers: json["golfers"].map((golfer) => Golfer.fromJson(golfer)).toList()
with
golfers: json["golfers"].map<Golfer>((golfer) => Golfer.fromJson(golfer)).toList()
I have two controller FooController and BooController (the last is for a backward compatibility), I want only the FooController will return its model with upper camel case notation ("UpperCamelCase").
For example:
public class MyData
{
public string Key {get;set;}
public string Value {get;set;}
}
public class BooController : ControllerBase
{
public ActionResult<MyData> GetData() { ... }
}
public class FooController : ControllerBase
{
public ActionResult<MyData> GetData() { ... }
}
The desired GET output:
GET {{domain}}/api/Boo/getData
[
{
"key": 1,
"value": "val"
}
]
GET {{domain}}/api/Foo/getData
[
{
"Key": 1,
"Value": "val"
}
]
If I'm using the AddJsonOptions extension with option.JsonSerializerOptions.PropertyNamingPolicy = null like:
services.AddMvc()
.AddJsonOptions(option =>
{
option.JsonSerializerOptions.PropertyNamingPolicy = null;
});
both BooController and FooController returns the data with upper camel case notation.
How to make only the FooController to return the data in upper camel case notation format?
Add [JsonProperty("name")] in the response class property.
stackoverflow.com/a/34071205/5952008
Solved (although kind of bad performance and a hacky solution - hoping for someone to suggest a better solution):
Inside the legacy BooController I'm serializing the response just before returning it using a custom formater of JsonSerializerSettings:
public class BooController : ControllerBase
{
public ActionResult<MyData> GetData()
{
var formatter = JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return Ok(JsonConvert.SerializeObject(response, Formatting.Indented, formatter()));
}
}
Result in:
GET {{domain}}/api/Boo/getData
[
{
"key": 1,
"value": "val"
}
]
I have a simple POST request sending params using application/x-www-form-urlencoded encoding.
Looking in the wiremock docs I can't find a way to match the request by the params values, something like the querystring match I mean.
Furthermore it seems also impossible to contains for the body, nor to match the entire body in clear (just as base64).
Is there a way to match this kind of requests?
Another option that I found was to use contains for Stubbing Content-Type: application/x-www-form-urlencoded
{
"request": {
"method": "POST",
"url": "/oauth/token",
"basicAuthCredentials": {
...
},
"bodyPatterns": [
{
"contains": "username=someuser"
}
]
},
"response": {
....
}
}
With classic wiremock you can use bodyPatterns' matchers and regular expressions:
for example:
...
"request": {
"method": "POST",
"url": "/api/v1/auth/login",
"bodyPatterns": [
{
"matches": "(.*&|^)username=test($|&.*)"
},
{
"matches": "(.*&|^)password=123($|&.*)"
}
]
},
I had a similar problem - I wanted to check the exact parameters , but without patterm magic (so easier to maintain). As a workaround, I created a helper class :
import java.util.Iterator;
import java.util.LinkedHashMap;
public class WireMockUtil {
public static String toFormUrlEncoded(LinkedHashMap<String, String> map) {
if (map == null) {
return "";
}
StringBuilder sb = new StringBuilder();
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
String value = map.get(key);
appendFormUrlEncoded(key,value,sb);
if (it.hasNext()) {
sb.append('&');
}
}
return sb.toString();
}
public static String toFormUrlEncoded(String key, String value) {
StringBuilder sb = new StringBuilder();
appendFormUrlEncoded(key, value,sb);
return sb.toString();
}
public static void appendFormUrlEncoded(String key, String value, StringBuilder sb) {
sb.append(key).append('=');
if (value != null) {
sb.append(value);
}
}
}
Inside the Wiremock test you can use it via:
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
...
withRequestBody(equalTo(WireMockUtil.toFormUrlEncoded(map))).
Or check only dedicated parts by containing:
withRequestBody(containing(WireMockUtil.toFormUrlEncoded("key","value1"))).
You could try https://github.com/WireMock-Net/WireMock.Net
Matching query parameters and body can be done with this example json:
{
"Guid": "dae02a0d-8a33-46ed-aab0-afbecc8643e3",
"Request": {
"Url": "/testabc",
"Methods": [
"put"
],
"Params": [
{
"Name": "start",
"Values": [ "1000", "1001" ]
},
{
"Name": "end",
"Values": [ "42" ]
}
],
"Body": {
"Matcher": {
"Name": "WildcardMatcher",
"Pattern": "test*test"
}
}
}
}
I am trying to use spring-data-elasticsearch to set up mappings for a type that is equivalent to the json configuration below and I am running into issues.
{
"_id" : {
"type" : "string",
"path" : "id"
},
"properties":{
"addressName":{
"type":"multi_field",
"fields":{
"addressName":{
"type":"string"
},
"edge":{
"analyzer":"edge_search_analyzer",
"type":"string"
}
}
},
"companyName":{
"type":"multi_field",
"fields":{
"companyName":{
"type":"string"
},
"edge":{
"analyzer":"edge_search_analyzer",
"type":"string"
}
}
}
}
}
Here is the entity class as it stands now:
#Document(indexName = "test", type="address")
#Setting(settingPath="/config/elasticsearch/test.json") // Used to configure custom analyzer/filter
public class Address {
#Id
private Integer id;
#MultiField(
mainField = #Field(type = FieldType.String),
otherFields = {
#NestedField(dotSuffix = "edge", type = FieldType.String, indexAnalyzer = "edge_search_analyzer")
}
)
private String addressName;
#MultiField(
mainField = #Field(type = FieldType.String),
otherFields = {
#NestedField(dotSuffix = "edge", type = FieldType.String, indexAnalyzer = "edge_search_analyzer")
}
)
private String companyName;
public Address() {}
public Address(AddressDTO dto) {
id = dto.getId();
addressName = dto.getAddressName();
companyName = dto.getCompanyName();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
Preconditions.checkNotNull(id);
this.id = id;
}
public String getAddressName() {
return addressName;
}
public void setAddressName(String addressName) {
this.addressName = addressName;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
}
What I am finding is that this results in a mapping like:
"address" : {
"properties" : {
"addressName" : {
"type" : "string",
"fields" : {
"addressName.edge" : {
"type" : "string",
"index_analyzer" : "edge_search_analyzer"
}
}
},
"companyName" : {
"type" : "string",
"fields" : {
"companyName.edge" : {
"type" : "string",
"index_analyzer" : "edge_search_analyzer"
}
}
}
}
}
Which results in two issues:
Id mapping is not done so Elasticsearch generates the ids
Searches on the properties with the custom analyzer do not work properly
If I add the following to override the use of the annotations everything works fine:
#Mapping(mappingPath="/config/elasticsearch/address.json")
Where "address.json" contains the mapping configuration from the top of this post.
So, can anyone point out where I might be going wrong with what I'm trying to achieve?
Is is possible using available annotations or am I going to have to stick with json configuration?
Also, is there a "correct" way to setup index-level configuration via spring-data-elasticsearch, or is that not a supported approach? Currently, I am using the #Setting annotation for this purpose