I have a JPA domain entity that I'm updating from user input. Depending on lots of factors about that object, it has actions to perform when it's updated (in this case, the update is to "mark it completed").
In the database, two "post-completion action configuration" fields (note and next_workflow) are nullable, and either have a value of interest, or are NULL. There may be very many of these, but I'm starting with these two.
I wrote the following method in the model class:
#PostUpdate
public void gotUpdate() {
System.out.println("Got post update for " + this.getDescription());
if (! this.getNote().isEmpty()) {
Note n = new Note();
n.setAssetId(this.getAssetId());
n.setNotifyLessor(1);
n.setNote(this.getLessorNote() + this.getCapturedData());
n.setCreatedDate(new Date());
n.persist();
}
System.out.println("In the middle of post update for " + this.getDescription());
if (this.getNextWorkflow() != 0) {
Asset a = this.getAssetId();
a.setWorkflowId(Workflow.findWorkflow(this.getNextWorkflow()));
a.merge();
}
System.out.println("Finishing post update for " + this.getDescription());
}
For entities with NULL "note" values, the console output is:
Got post update for this item
For entities with non-NULL "note" values and with NULL "nextWorkflow" values, the console output is:
Got post update for this item
In the middle of post update for this item
No errors anywhere, no stack dump, no nothing. The method just silently quits, AND the merge I'm doing on this entity doesn't complete (the database remains unchanged).
Stepping through this in the debugger gets to that line where things are being tested, and if the value is NULL, it pops a tab saying "Source not found", which I don't really know what to make of. I think that's just the debugger saying it can't step into something, but I'm not actually asking it to...
Rookie question, it turns out.
If Object's field field is null, then object.getField().length() is a call to a null pointer.
The answer is to test the field for nullness, not some side-effect of nullness.
if (! (this.getNote() == null)) {
...
}
Related
I'm running into a weird problem while writing and testing my Firestore rules. Here's what I want to achieve:
When the application starts, the user gets logged in anonymously. The
user starts a new game.
I create a 'Session' that basically consists of just a timestamp.
The player plays the game, gets a certain highscore and goes to a screen where the score can be sent to the global highscore list. When the highscore is submitted, I check if there's an existing session for this player and if the time that has passed is long enough for the highscore to be considered valid.
On the client (javascript) I use the following line to send the timestamp in my documents:
firebase.firestore.FieldValue.serverTimestamp()
This is the current ruleset. You can see that a score can only be created when the createdAt of the new higscore is later than the createdAt of the session.
service cloud.firestore {
match /databases/{database}/documents {
function isValidNewScoreEntry() {
return request.resource.data.keys().hasOnly(['createdAt', 'name', 'score']) &&
request.resource.data.createdAt is timestamp &&
request.resource.data.name is string &&
request.resource.data.score is int &&
request.resource.data.name.size() <= 20
}
match /highscores/{entry} {
allow list: if request.query.limit <= 10;
allow get: if true;
allow create: if isValidNewScoreEntry() &&
request.resource.data.createdAt > get(/databases/$(database)/documents/sessions/$(request.auth.uid)).data.createdAt;
}
function isValidNewSession() {
return request.resource.data.keys().hasOnly(['createdAt']) &&
request.resource.data.createdAt is timestamp
}
match /sessions/{entry} {
allow list: if false;
allow get: if false;
allow create: if isValidNewSession();
allow update: if isValidNewSession();
}
}
}
When I simulate/test these rules, I get an error that says that I cannot compare a 'timestamp' to a 'map'. I don't know why the 'createdAt' value is a map, but it seems like the get() method returns something different than expected.
My question is: What would be the correct way to compare the property createdAt from the newly submitted entry to the property createdAt of the existing session document, like I'm trying to do in the rules described above.
This is what a'Score' entry look like
This is what a 'Session' entry looks like
EDIT:
I've done some more digging, and found that this line works:
if request.resource.data.createdAt.toMillis() > get(/databases/$(database)/documents/sessions/$(request.auth.uid)).data.createdAt.seconds * 1000;
This makes it pretty clear that not both createdAt are the same format. The last one seems to be a basic object with the properties 'seconds' and 'nanoseconds'. I'm sure it stems from the Timestamp interface, but it gets returned as a flat object since none of the methods found here exist and give an error when trying to call them. The property 'seconds' however does exists on the second timestamp, but is not accessible on the first one.
I've found out why the timestamp is not what I expected and got cast to a 'map'.
After digging through the documentation I found that the get() method returns a resource. resource has a property data: a map. So the get() method does not return a document as I expected but a flat JSON object that gives me all properties found in de database.
https://firebase.google.com/docs/reference/rules/rules.firestore
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
I don't understand why it is recommended everywhere to use AddOrUpdate in the Seed method?
We develop application for half a year already and the AddOrUpdates overwrites user changes every time we update the server. E.g. if we call in the Seed:
context.Styles.AddOrUpdate(new Style { Id = 1, Color = "red" });
And user changes the Style to "green" then on next server update we overwrite it to "red" again and we get very annoyed user.
It looks that if we change AddOrUpdate to Add we will be guaranteed from overwriting user data. If we still need some special case we can put it to separate migration. Unlike the general Configuration.Seed method particular migrations don't run twice over the same database version.
I assume that Style's primary key is Id. The overload of AddOrUpdate that you use only checks if there is a record having Id == 1. If so, it updates it. That's all.
What's going wrong here is that the primary key is a surrogate key, i.e. it's there for querying convenience, but it's got no business meaning. Usually, with migrations you want to look for the natural keys of entities though. That's how the user identifies data. S/he wants a green style, not a style identified by 1.
So I think you should use this overload of AddOrUpdate:
context.Styles.AddOrUpdate( s => s.Color,
new Style { Id = 1, Color = "red" });
Now when there is no red style anymore, a new one is inserted, overriding the Id value (assuming that it's generated by the database).
From your later comments I understand that you want to Add data when they're new, but not update them when they exist (compared by primary key). For this you could use a slightly adapted version of an AddWhenNew method I described here. For your case I would do it like so:
public T void MarkAsAddedWhenNew<T>(this DbContext context,
Expression<Func<T, object>> identifierExpression, T item)
where T : class
{
context.Set<T>().AddOrUpdate(identifierExpression, item);
if (context.Entry(item).State != System.Data.Entity.EntityState.Added)
{
var identifierFunction = identifierExpression.Compile();
item = context.Set<T>()
.Local
.Single(x => identifierFunction(item)
.Equals(identifierFunction(x)));
context.Entry(item).State = System.Data.Entity.EntityState.Unchanged;
}
return item;
}
Re-fetching the item from the local collection is a nuisance, but necessary because of a bug in AddOrUpdate(). This bug also caused the error you got when setting the state of the original entry to Unchanged: it was a different instance than the attached one.
The way Add method acts is misleading. It Inserts data into database even if there is already a row with the same PrimaryKey as we do Add. It just creates new PrimaryKey ignoring our value silently. I should have tried it before asking the question, but anyway, I think I'm not the only one who confused by this. So, in my situation Add is even worse than AddOrUpdate.
The only solution I've come to is following:
public static void AddWhenNew<T>(this DbContext ctx, T item) where T : Entity
{
var old = ctx.Set<T>().Find(item.Id);
if (old == null)
ctx.Set<T>().AddOrUpdate(item);
/* Unfortunately this approach throws exception when I try to set state to Unchanged.
Something like:"The entity already exists in the context"
ctx.Set<T>().AddOrUpdate(item);
if (ctx.Entry(item).State != System.Data.Entity.EntityState.Added)
ctx.Entry(item).State = System.Data.Entity.EntityState.Unchanged;
*/
}
I'm trying to do this with mongodbauthmanager. I'm follow step by step in Usage section but finally i'm getting PHP warning: Illegal offset type. I had posted this question at Yii Extension before clone to SO:
Please tell me what is wrong?
1// Config
'authManager'=>array(
'class' =>'CMongoDbAuthManager',
'showErrors' => true,
),
2// Create auth items in db
$auth = new CMongoDbAuthManager();
$bizRule = 'return Yii::app()->user->id==$params["User"]->_id;';
$auth->createTask('updateSelf', 'update own information', $bizRule);
//I had tried with $auth->createOperation() but they has the same error
$role = $auth->createRole('user');
$role->addChild('updateSelf');
$auth->save();
and here is result in db
result in db http://i.minus.com/iIpXoBlDxaEfo.png
**3// Checking access in controller ** - UPDATE CODE AND ERROR
public function actionUpdate($id)
{
$model=$this->loadModel($id);
$params = array('User'=>$model);
if (!Yii::app()->user->checkAccess('updateSelf', Yii::app()->user->id,$params) )
{
throw new CHttpException(403, 'You are not authorized to perform this action');
}
//another statement ...
}
4// Getting error:
Fatal error : Cannot use object of type MongoId as array in F:\Data\03. Lab\www\yii\framework\web\auth\CAuthManager.php(150) : eval()'d code on line 1
RESOLVED PROBLEM
Base-on the answer of #Willem Renzema, I resolve my problem. Now, I update here and hope it useful for someone have this error.
0// First, config authManager with defaultRoles
'authManager'=>array(
'class'=>'CMongoDbAuthManager',
'showErrors' => true,
'defaultRoles'=> array('user'),//important, this line help we don't need assign role for every user manually
),
1// Fix save id in UserIdentity class
class UserIdentity extends CUserIdentity
{
private $_id;
//...
public function authenticate()
{
//...
$this->_id = (string)$user->_id;//force $this save _id by string, not MongoId object
//...
}
//...
}
2// Fix $bizrule in authe items
($bizrule will run by eval() in checkAccess)
//use _id as string, not MongoId object
$bizRule = 'return Yii::app()->user->id==(string)$params["User"]->_id;';
3// And user checkAccess to authorization
public function actionUpdate($id){
/**
* #var User $model
*/
$model=$this->loadModel($id);
$params = array('User'=>$model);
if (!Yii::app()->user->checkAccess('updateSelf', $params) )
{
throw new CHttpException(403, 'You are not authorized to perform this action');
}
//...
}
4// Done, now we can use checkAccess :D
First off, your original use of checkAccess was correct. Using Yii::app()->user->checkAccess() you are using the following definition:
http://www.yiiframework.com/doc/api/1.1/CWebUser#checkAccess-detail
Now, CWebUser's implementation of checkAccess calls CPHPAuthManager's implementation, which is where you encountered your problem with an illegal offset type.
http://www.yiiframework.com/doc/api/1.1/CPhpAuthManager#checkAccess-detail
An Illegal offset type means you are attempting to access an array element by specifying its key (also known as: offset) with a value that doesn't work as a key. This could be another array, an object, null, or possibly something else.
Your stack trace posted on the extensions page reveals that the following line gives the problem:
if(isset($this->_assignments[$userId][$itemName]))
So we have two possibilities for the illegal offset: $userId and $itemName.
Since $itemName is clearly a string, the problem must be with $userId.
(As a side note, the fact that your stack trace revealed surrounding code of this error also revealed that, at least for CPHPAuthManager, you are using a version of Yii that is prior to 1.1.11. Observe that lines 73 and 74 of https://github.com/yiisoft/yii/blob/1.1.11/framework/web/auth/CPhpAuthManager.php do not exist in your file's code.)
At this point I would have guessed that the problem is that the specified user is not logged in, and so Yii::app()->user->id is returning null. However, the new error you encountered when placing Yii::app()->user->id as the 2nd parameter of checkAccess reveals something else.
Since the 2nd parameter is in fact what should be the $params array that appears in your bizRule. Based on the error message, this means that Yii::app()->user->id is returning a mondoId type object.
I was unfamiliar with this type of object, so looked it up:
http://php.net/manual/en/class.mongoid.php
Long story short, you need to force Yii::app()->user->id to return the string value equivalent of this mondoId object. This likely set in your UserIdentity class in the components folder. To force it to be a string, simply place (string) to force a type conversion.
Example:
$this->_id = (string)$User->_id;
Your exact code will vary, based on what is in your UserIdentity class.
Then, restore your checkAccess to the signature you had before, and it should eliminate the Illegal offset error you encountered originally.
Note however that I have not used this extension, and while performing the following actions should fix this issue, it may cause new issues if the extension relies on the fact that Yii::app()->user->id is a mondoId object, and not a string.
I am using rich faces select component.
I want dynamic values when user manually type some thing in the select component.
<rich:select enableManualInput="true" defaultLabel="start typing for select" value="#{supplierSearchBean.userInput}">
<a4j:ajax event="keyup" execute="#this" listener="#{supplierSearchBean.userInputChange}"/>
<f:selectItems value="#{supplierSearchBean.selectOptions}" />
</rich:select>
Java code as follows
public void userInputChange(ActionEvent ae){
Map map = ae.getComponent().getAttributes();
System.out.println(map.toString());
}
public void setUserInput(String userInput) {
System.out.println("userINput = " + userInput);
this.userInput = userInput;
}
Here i found 2 issues
1st: setUserINput always print empty string when user type value
2nd: listener method never get call.
any help ?
The problem is most probably that there is no selected value while the user types, and this component restricts the allowed values to the specified select items. A partial input is thus not valid and cannot be bound to your bean.
I think you could get the expected behavior if you use a rich:autocomplete instead. However, if you want to restrict the allowed values, maybe you can keep your rich:select and listen for the selectitem event.
Override getItems function in richfaces-utils.js file in richfaces-core-impl-4.0.0.Final.jar under richfaces-core-impl-4.0.0.Final\META-INF\resources folder.
Change the condition of pushing items to be
if(p != -1)
instead of
if(p == 0)
This should fix the issue.
Framework: I'm using using MVC 3 + EntityFramework 4.1 Code-First.
Concept: One Legislation entity has many Provision entities. The idea is that the user enters a Legislation entity, that gets saved then the function that saves it passes it along to another function to see whether that Legislation has a ShortTitle. If it does, then it formats it into a properly worded string and includes it as the Legislation's first Provision, then saves the changes to db.
Issue: The problem is, I've tried coding it in different ways, I keep getting a NullReferenceException, telling me to create a new object instance with the "new" keyword, and points me to the savedLegislation.Provisions.Add(provision); line in my second function.
Here are the two functions at issue, this first one saves the Legislation proper:
public Legislation Save(NewLegislationView legislation)
{
Legislation newLegislation = new Legislation();
// Simple transfers
newLegislation.ShortTile = legislation.ShortTile;
newLegislation.LongTitle = legislation.LongTitle;
newLegislation.BillType = legislation.BillType;
newLegislation.OriginatingChamber = legislation.OriginatingChamber;
newLegislation.Preamble = legislation.Preamble;
// More complicated properties
newLegislation.Stage = 1;
this.NumberBill(newLegislation); // Provides bill number
newLegislation.Parliament = db.LegislativeSessions.First(p => p.Ending >= DateTime.Today);
newLegislation.Sponsor = db.Members.Single(m => m.Username == HttpContext.Current.User.Identity.Name);
// And save
db.Legislations.Add(newLegislation);
db.SaveChanges();
// Check for Short titles
this.IncludeShortTitle(newLegislation);
// return the saved legislation
return newLegislation;
}
And the second function which is invoked by the first one deals with checking whether ShortTitle is not empty and create a Provision that is related to that Legislation, then save changes.
public void IncludeShortTitle(Legislation legislation)
{
var savedLegislation = db.Legislations.Find(legislation.LegislationID);
if (savedLegislation.ShortTile.Any() && savedLegislation.ShortTile.ToString().Length >= 5)
{
string shortTitle = "This Act may be cited as the <i>" + savedLegislation.ShortTile.ToString() + "</i>.";
var provision = new Provision()
{
Article = Numbers.CountOrNull(savedLegislation.Provisions) + 1,
Proponent = savedLegislation.Sponsor,
Text = shortTitle
};
savedLegislation.Provisions.Add(provision);
db.SaveChanges();
}
}
I've been researching how SaveChanges() works and whether it is properly returning the updated entity, it does (since I get no issue looking it up in the second function). If it works properly, and the legislation is found and the provision is newly created in the second function, I don't see what is the "null" reference it keeps spitting out.
The null reference in this case would be savedLegislation.Provisions. The Provisions collection won't be initialized to a new List<Provision> when EF returns your Legislation instance from the db.Legislations.Find(...) method.
The first thing I'd try is something like this:
var savedLegislation = db.Legislations
.Include("Provisions")
.First(l => l.LegislationID == legislation.LegislationID);
... but I'd also consider just using the legislation instance that was passed into the method rather than fetching it from the database again.