Relationship property not persisted in graph - spring-data-neo4j

Context
SDN 3.3.0.RELEASE / 3.4.0.M1
Neo4j 2.1.7 in distant server mode.
Use case
I have an existing Person in database which can have multiple PersonTranslatedContent that can be in several languages.
So basicaly, my modelisation is like :
(:Person)-[:TRANSLATION {lang:"fr}]->(:PersonTranslatedContent)
Problem
When I create a PersonTranslatedContent node and a TRANSLATION relation to link with my Person, the 'lang' property is not persisted on the relationship.
The nodes are corecctly created, but when I query the database from the Neo4j browser, my relationship has only one property : _type__ : PersonToPersonTranslatedContent
Analysis
When logging the HTTP request received by Neo4j, the requets performed are in this order :
1. MATCH (n) WHERE id(n) = {id_n} MATCH (m) WHERE id(m) = {id_m} CREATE (n)-[r:`TRANSLATION`]->(m) SET r={props} RETURN id(r) as id, type(r) as type, r as properties, id(startNode(r)) as start, id(endNode(r)) as end
2. START r=rel({id}) SET r.`_ _type_ _` = {value}
3. START r=rel({id}) RETURN id(r) as id, type(r) as type, r as properties, id(startNode(r)) as start, id(endNode(r)) as end
4. **START r=rel({id}) SET r.`lang` = {value}** <-- here the lang property seems to be correctly set !
5. START r=rel({id}) SET r = {props} <- here, props = {"_ _type_ _" : PersonToPersonTranslatedContent"}
All those REST calls are done within a simple call to personToPersonTranslatedContentRepository.save(). I followed the white rabit in debug mode and here is the shortened call stack :
Neo4jEntityConverterImpl.write() --> entityStateHandler.useOrCreateState() --> RestAPICypherImpl.createRelationship() (correspond to bullet 1)
Neo4jEntityConverterImpl.write() --> typeMapper.writeType() --> RestAPICypherImpl.setPropertyOnEntity() (correspond to bullet 2)
Neo4jEntityConverterImpl.write() --> sourceStateTransmitter.copyPropertiesTo() --> persistentEntity.doWithProperties() --> RestAPICypherImpl.setPropertyOnEntity() (correspond to bullet 4)
Neo4jEntityConverterImpl.write() --> sourceStateTransmitter.copyPropertiesTo() --> ((UpdateableState)target).flush() --> RestAPICypherImpl.setPropertiesOnEntity() (correspond to bullet 5)
So, in my opinion considering what I know and what I saw during debug, the problem seems to be around the "propertyData" attribute of class RestEntity which is used in the ((UpdateableState)target).flush() ! It always hold the value {"_ type _" : PersonToPersonTranslatedContent"} but never contains my "lang" property.
Note : my problem is the same as the one explain here 3.3.0.M1 : Properties on RelationShipEntity not saved when using CypherRestGraphDatabase?. His post has no satisfying answer.
Can you help me (and him I guess) ?
Thx :)
13/07/15 : Updated
I finally manage to resolve my problem by using :
Map<String, Object> properties = new HashMap<>();
properties.put("lang", lang);
properties.put("__type__", "UserToUserTranslatedContent");
neo4jOperations.createRelationshipBetween(neo4jOperations.getNode(user.getId()), neo4jOperations.getNode(translatedContent.getId()), RelationNames.TRANSLATION, properties);
But it is still strange that simple save() operation do not work as expected

For people looking for a fix, I made a report concerning this problem (https://jira.spring.io/browse/DATAGRAPH-699) and a quick way to use SDN like before (< 3.3.x) is doing like this:
remove "spring-data-neo4j" and "spring-data-neo4j-rest" from your build.gradle and add these lines:
repositories {
maven {
url "https://jitpack.io"
}
}
dependencies {
compile 'org.springframework:spring-orm:4.1.7.RELEASE'
compile 'org.springframework:spring-aspects:4.1.7.RELEASE'
compile 'com.github.nousmotards:spring-data-neo4j:3.3.1.NM1'
}
Hope this will help in the mean time of having a real fix ;)

Related

spring data neo4j (SDN4) - find by relationship

I'm studying spring data for Neo4J and I've seen some examples where you just define a method in the repository interface following some standards (to find by a specific attribute) and it's automatically handled by spring. Ex: findByName.
It works quite straightforward with basic attributes but it doesn't seems to work when the attribute is actually a relationship.
See this example:
public class AcceptOrganizationTask extends AbstractTask {
#Relationship(type="RELATES_TO", direction = "OUTGOING")
private OrganizationInvite invitation;
...
}
In the repository interface I've defined 3 methods (All with the same goal):
List<AcceptOrganizationTask> findAllByInvitation(OrganizationInvite invite);
#Query("MATCH (i:OrganizationInvite)<-[RELATES_TO]-(t:AcceptOrganizationTask) WHERE i={invite} RETURN t")
List<AcceptOrganizationTask> getTaskByInvitation(#Param("invite") OrganizationInvite invite);
AcceptOrganizationTask findByInvitation_Id(Long invitationId);
None of them are able to retrieve the task by its invite property. But if I use use findAll() I can get the object with the property associated to the correct invitation.
Am I missing something ?
Bellow I have the Cypher code generated for this three methods:
findAllByInvitation
MATCH (n:`AcceptOrganizationTask`)
WHERE n.`invitation` = { `invitation_0` }
WITH n MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n)
with params {invitation_0={entityId=15, version=0, createdOn=1484758262374, lastChanged=1484758262374, createUser=null, lastUpdatedBy=null, email=user2#acme.com, randomKey=fc940b14-12c3-4894-b2b4-728e3a6b8036, invitedUser={entityId=11, version=0, createdOn=1484758261450, lastChanged=1484758261450, createUser=null, lastUpdatedBy=null, name=User user2#acme.com, email=user2#acme.com, credentialsNonExpired=true, lastPasswordResetDate=null, authorities=null, authoritiesInDB=[], accountNonExpired=true, accountNonLocked=true, enabled=true, id=11}, id=15}}
getTasksByInvitation
MATCH (i:OrganizationInvite)<-[RELATES_TO]-(t:AcceptOrganizationTask)
WHERE i={invite} RETURN t with params {invite=15}
findByInvitation_Id
MATCH (n:`AcceptOrganizationTask`)
MATCH (m0:`OrganizationInvite`)
WHERE m0.`id` = { `invitation_id_0` }
MATCH (n)-[:`RELATES_TO`]->(m0)
WITH n
MATCH p=(n)-[*0..1]-(m) RETURN p, ID(n)
with params {invitation_id_0=15}
All entities inherit from a common AbstractEntity with and Long id field, annotated with #GraphId.
Am I missing something ?
At the moment derived finder queries support only works to one level of nesting.
We are hoping to add this feature soon in the coming weeks though.
For now you can write a custom #Query to get around this.

Getting text in <td> element using SpecFlow and Selenium

I am getting an error in my SpecFlow unit test:
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
at TechTalk.SpecFlow.SpecFlowContext.Get[T](String key)
at F*****.Live.PS.*******.Steps.*******.EnterStagingDateAndSaveSteps.ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(String p0) in c:\Git\LivePeopleSystemTests\Fourth.Live.PS.AutomationTests\F*****.Live.PS.*******.Steps*******\EnterStagingDateAndSaveSteps.cs:line 175
Here is the referenced method in EnterStagingDateAndSaveSteps.cs
[Then(#"user sees ""(.*)"" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module")]
public void ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(string p0)
{
// GetNextReEnrollmentWindow
string validator =
ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow")
.GetNextReEnrollmentWindow();
Assert.IsTrue(validator == p0);
}
(I also tried return Driver.WaitAndGetText(_nextReEnrollmentWindow); in there)
so it looks like the key _nextReEnrollmentWindow doesn't exist, but here it is defined in PensionAssessmentDates.cs:
private readonly By _nextReEnrollmentWindow = By.Id("NextReEnrollmentWindow");
The PensionAssessmentSteps is set into the ScenarioContext.Current like this:
[Then(#"user sees ""(.*)"" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module")]
public void ThenUserSeesInNEXTRE_ENROLMENTWINDOWTextFieldInPENSIONASSESSMENTDATESPageInPENSIONModule(string p0)
{
// GetNextReEnrollmentWindow
string validator =
ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow")
.GetNextReEnrollmentWindow();
Assert.IsTrue(validator == p0);
}
and here is the actual web-page I am trying to test showing the element exists:
I'd be grateful for any pointers or advice on what I have missed which is causing my unit test to stop as below:
EnterStagingDateAndSaveSteps.WhenUserClicksSAVEButtonUnderAUTOMATICENROLMENTATSTAGINGDATEFrameInPENSIONASSESSMENTDATESPageInPENSIONModule() (0.7s)
Then user sees "02 Oct 2019 to 02 Apr 2020" in NEXT RE-ENROLMENT WINDOW Text Field in PENSION ASSESSMENT DATES page in PENSION module
-> error: The given key was not present in the dictionary.
The ScenarioContext.Current.Get and Set methods simply allow you to add an object to the dictionary of values attached to the current scenario using a key.
Once you have added an element to the dictionary in one step, you can retrieve it in another step. The exception you are getting implies that you have not added anything to the dictionary using the key _nextReEnrollmentWindow.
Do you call ScenarioContext.Current.Set("_nextReEnrollmentWindow", something); anywhere in your code? I suspect not.
Given the way you have asked the question I'm suspecting that you expect the ScenarioContext.Current.Get<PensionAssessmentDates>("_nextReEnrollmentWindow") call to get you the current instance of the page object of type PensionAssessmentDates and then get the element using the selector _nextReEnrollmentWindow. This is not how it works.
you want to do one of two things I believe. Either add your page object PensionAssessmentDates to the ScenarioContext.Current and then get the page object out and call the method which uses the private field _nextReEnrollmentWindow.
Or (much better in my opinion) ditch your use of the ScenarioContext.Current altogether and instead create objects which hold your page objects and let Specflows internal DI framework provide those to your step classes using context injection.

Annotating a document with JAPE

I have been searching for a solution to this for weeks, I have some documents(about 95) that I am trying to classify using GATE. I have put them in one corpus I called training_corpus, however, after ANNIE has annotated the corpus, I have to go back into each file, select all token in the document, and create an annotation called Mention, with feature type and value the class for the document. for example:
type Start End id Features
Mention 0 70000 2588 {type=neg}
Is there anyway to automatically do this with JAPE? Basically, I want to select all tokens and create a new annotation with feature(type=class). Also, the class is appended to the document. Since there are many documents, can JAPE extract the class from the document name and set it to the value of Mentions feature. Example document name is neg_data1.txt, so the annotation will be Mention.type = neg?
Any help will be greatly appreciated. Thanks
I think you answered to your question by yourself.If the class assignment based on just a token present in text - why not simply process text outside of GATE?
For example to create an xml file like:
text and then use it in training process.
Also you can create a simple JAPE rule which will:
a) will take a text within document boundaries (see gate.Utils.length methods AFAIR)
b) based on presence of your token will create a new Annotation instance with features necessary.
an abstract example:
Phase: Instance
Input: Token
Options: control = once
Rule:Instance
(
{Token}
):instance
-->
{
AnnotationSet instances = outputAS.get("INSTANCE_ANNOTATION");
FeatureMap featureMap = Factory.newFeatureMap();
if (instances!=null&&!instances.isEmpty()){
featureMap.put("features when annotation presented in doc");
}else{
featureMap.put("features when annotation not in doc");
}
outputAS.add(new Long(0), new Long(documentLength), "Mention", featureMap);
}

Sitecore Multisite Manager and 'source' field in template builder

Is there any way to parametise the Datasource for the 'source' field in the Template Builder?
We have a multisite setup. As part of this it would save a lot of time and irritation if we could point our Droptrees and Treelists point at the appropriate locations rather than common parents.
For instance:
Content
--Site1
--Data
--Site2
--Data
Instead of having to point our site at the root Content folder I want to point it at the individual data folders, so I want to do something like:
DataSource=/sitecore/content/$sitename/Data
I can't find any articles on this. Is it something that's possible?
Not by default, but you can use this technique to code your datasources:
http://newguid.net/sitecore/2013/coded-field-datasources-in-sitecore/
You could possibly use relative paths if it fits with the rest of your site structure. It could be as simple as:
./Data
But if the fields are on random items all over the tree, that might not be helpul.
Otherwise try looking at:
How to use sitecore query in datasource location? (dynamic datasouce)
You might want to look at using a Querable Datasource Location and plugging into the getRenderingDatasource pipeline.
It's really going to depend on your use cases. The thing I like about this solution is there is no need to create a whole bunch of controls which effectively do he same thing as the default Sitecore ones, and you don't have to individually code up each datasource you require - just set the query you need to get the data. You can also just set the datasource query in the __standard values for the templates.
This is very similar to Holger's suggestion, I just think this code is neater :)
Since Sitecore 7 requires VS 2012 and our company isn't going to upgrade any time soon I was forced to find a Sitecore 6 solution to this.
Drawing on this article and this one I came up with this solution.
public class SCWTreeList : TreeList
{
protected override void OnLoad(EventArgs e)
{
if (!String.IsNullOrEmpty(Source))
this.Source = SourceQuery.Resolve(SContext.ContentDatabase.Items[ItemID], Source);
base.OnLoad(e);
}
}
This creates a custom TreeList control and passes it's Source field through to a class to handle it. All that class needs to do is resolve anything you have in the Source field into a sitecore query path which can then be reassigned to the source field. This will then go on to be handled by Sitecore's own query engine.
So for our multi-site solution it enabled paths such as this:
{A588F1CE-3BB7-46FA-AFF1-3918E8925E09}/$sitename
To resolve to paths such as this:
/sitecore/medialibrary/Product Images/Site2
Our controls will then only show items for the correct site.
This is the method that handles resolving the GUIDs and tokens:
public static string Resolve(Item item, string query)
{
// Resolve tokens
if (query.Contains("$"))
{
MatchCollection matches = Regex.Matches(query, "\\$[a-z]+");
foreach (Match match in matches)
query = query.Replace(match.Value, ResolveToken(item, match.Value));
}
// Resolve GUIDs.
MatchCollection guidMatches = Regex.Matches(query, "^{[a-zA-Z0-9-]+}");
foreach (Match match in guidMatches)
{
Guid guid = Guid.Parse(match.Value);
Item queryItem = SContext.ContentDatabase.GetItem(new ID(guid));
if (item != null)
query = query.Replace(match.Value, queryItem.Paths.FullPath);
}
return query;
}
Token handling below, as you can see it requires that any item using the $siteref token is inside an Site Folder item that we created. That allows us to use a field which contains the name that all of our multi-site content folders must follow - Site Reference. As long at that naming convention is obeyed it allows us to reference folders within the media library or any other shared content within Sitecore.
static string ResolveToken(Item root, string token)
{
switch (token)
{
case "$siteref":
string sRef = string.Empty;
Item siteFolder = root.Axes.GetAncestors().First(x => x.TemplateID.Guid == TemplateKeys.CMS.SiteFolder);
if (siteFolder != null)
sRef = siteFolder.Fields["Site Reference"].Value;
return sRef;
}
throw new Exception("Token '" + token + "' is not recognised. Please disable wishful thinking and try again.");
}
So far this works for TreeLists, DropTrees and DropLists. It would be nice to get it working with DropLinks but this method does not seem to work.
This feels like scratching the surface, I'm sure there's a lot more you could do with this approach.

How to generate media item link with id instead of path in Sitecore

Anyone knows how to generate links in sitecore with ID instead of item path?
If you use GetMediaUrl method from the API, I can get this URL:
/~/media/Images/Archive/content/News and Events/News_and_Events_Level2/20070419162739/iwhiz3.jpg
The problem with this approach is that if someone changes the media item name, removes it somewhere or deletes it, the above link will break.
I notice if I insert a media link from rich text editor, I get the link as below:
/~/media/14BDED00E4D64DFD8F74019AED4D74EB.ashx
The second link is better because it's using the item id, so if the actual media item is renamed, removed, or deleted, all related links will be updated too. On top of that, when Sitecore renders the page, it will actually convert the above link and display the item path so it's readable.
I'm using Sitecore 6.5 and currently doing content migration so I need to make sure all internal links are updated properly.
May I know if there is a method to generate the second link by using sitecore API?
Thanks!
The GetMediaItemUrl extension method seems to give you what you want.
public static class ItemExtensions
{
public static string GetMediaItemUrl(this Item item)
{
var mediaUrlOptions = new MediaUrlOptions() { UseItemPath = false, AbsolutePath = true };
return Sitecore.Resources.Media.MediaManager.GetMediaUrl(item, mediaUrlOptions);
}
}
[TestFixture]
public class when_using_items_extensions
{
[Test]
public void a_url_based_on_media_item_id_can_be_generated()
{
// Arrange
Database db = global::Sitecore.Configuration.Factory.GetDatabase("master");
Item item = db.GetItem("/sitecore/media library/Images/MyImage");
// Act
var mediaUrl = item.GetMediaItemUrl();
// Assert
Assert.That(mediaUrl, Is.EqualTo("/~/media/17A1341ABEEC46788F2159843DCEAB03.ashx"));
}
}
These are called dynamic links and you can normally generate them using the LinkManager e.g:
Sitecore.Links.LinkManager.GetDynamicUrl(item)
.. but I'm not sure of the method to do this with Media links (there probably is one but I cant seem to find it and its not on MediaManager) but the basic syntax is:
"/~/media/" + item.ID.ToShortID() + ".ashx"
If you always want to use ID's instead of paths, you can change this setting in webconfig to false (like this):
<setting name="Media.UseItemPaths" value="false"/>`
Here is what the webconfig describes about it:
MEDIA - USE ITEM PATHS FOR URLS
This setting controls if item paths are used for constructing media URLs.
If false, short ids will be used.
Default value: true
Then you can use the default implementation (without additional parameters):
Sitecore.Resources.Media.MediaManager.GetMediaUrl(item);
This is what I use:
var imgField = ((Sitecore.Data.Fields.ImageField)currentItem.Fields["Icon"]);
MediaUrlOptions opt = new MediaUrlOptions();
opt.AlwaysIncludeServerUrl = true;
// Absolute Path works as well. So either use AbsolutePath or AlwaysIncludeServerUrl
opt.AbsolutePath = true;
string mediaUrl = MediaManager.GetMediaUrl(imgField.MediaItem, opt);