import com.nabbr.player02.utilities.defaults.ConfigDefaults; /** * @author marcus geduld * @version 1.0 */ class com.nabbr.player02.utilities.PathEval { private static var NUMBER_FLAG:Number = 1000000000000; /* * EXAMPLED OBJECT var objSearchIn:Object = { first : "Ben", last : "White", friends : { kids : [ {first: "John", last: "Smith"} , { first : "Amy" , last: "Jones" } ], adults : [ {first: "Bill", last: "Green"} , { first : "Ted" , last: "Jennings" } ] } }; var strPath:String = "friends.kids[1].last"; trace( resolve(strPath, objSearchIn) ); //jones * */ //example: strPath = "friends.old[1].last" public static function resolve(strPath:String, objSearchIn:Object) : Object { //["friends","kids[1]","last"] var arrGrossSteps:Array = strPath.split("."); //["friends","kids", 1, "last"] var arrRefinedSteps:Array = findSteps(arrGrossSteps); //"Jones" var objResult:Object = usePathToGetValue(arrRefinedSteps,objSearchIn); return objResult; } /* * If you're trying to add data to a path, express the path using normal * dot syntax. For isntance, a plath could be "friends.school[1].last" * * There are some special rules for array indexes. In addition to noraml * integers, you can use negatives. Negative indexes count backwards from * the end of the array. So if the starting array colors is ["red","green","blue"], * you could replace "blue" with "yellow" via "colors[-1]". To replace "green", * use "colors[-2]" (or "colors[1]"). * * If you want to insert a value (as opposed to overwrite one), use this form * of indexing: "colors[1#i]" That means "add 'yellow' at index 1, using the * insert method ('i'). The pound sign is a separater between the index number * and the add method (in this casse 'i'). * * Note: paths can contain objects that don't exist. If the original object is * {name: "Bill Jones", phone: "(123) 123-1234"} a legal path is * "email.work" If you set the value to "bjones@xyz.com", the resulting * object will be * {name: "Bill Jones", phone: "(123) 123-1234", email: {work: "bjones@zyz.com"}} * */ //strAddMethod is for backward compatibility public static function setPathToValue(strPath:String, objValue: Object, objAddTo:Object, strAddMethod:String) : Void { //["friends","kids[1]","last"] var arrGrossSteps:Array = strPath.split("."); //["friends","kids", 1, "last"] var arrRefinedSteps:Array = findSteps(arrGrossSteps); usePathToSetValue(arrRefinedSteps,objValue, objAddTo, undefined,strAddMethod); } private static function usePathToSetValue(arrRefinedSteps:Array, objValue: Object, objAddTo:Object, objParent : Object, strAddMethod:String) : Void { var objIndex:Object = arrRefinedSteps.shift(); var booSpliceValue:Boolean = false; //if index is -1 or less, insert counting backwards from the end of the array if (typeof(objIndex) == "number") { //we need array indexes to be numbers. But to make the API usable, //I've allowed codes like 1#i (explained above) to be used as indexes. //If this class receives as index like that, it converts the index //to a really huge positive or negative number. That number is then //used as a flag to determine whether or not we're doing an insert //as opposed to an overwrite. if (objIndex <= -PathEval.NUMBER_FLAG || objIndex >= PathEval.NUMBER_FLAG) { booSpliceValue = true; //Now do some arithmetic to pull the actual index number //out of the huge number if (objIndex < 0) { objIndex += PathEval.NUMBER_FLAG; } else { objIndex -= PathEval.NUMBER_FLAG; } } //if the number is negative, use it to count backwards //from the end of the array if (objIndex < 0) objIndex = objAddTo[ "length" ] + objIndex; } //this will be needed to prep the container (see below) var objContainer:Object = objAddTo[ objIndex ]; //to help with prepping, we need to keep track of objAddTo[objIndex]'s parent objParent = objAddTo; //if the orgiginal object looks like this {a: 1, b:2} and we get a path //that looks like this "c.d.e[0]", we need to add new Objects and/or //Arrays to the original. prepContainer(objContainer, arrRefinedSteps, objAddTo, objIndex, booSpliceValue); //true = we're at the end of the path if (arrRefinedSteps.length == 0) { if (strAddMethod != undefined) { //NOTE: "head" and "tail" and "add" are for backward compatibility //with a method we used to add data to objects before //I wrote this class. Should we ever discontinue that //method, I can remove this whole if if (strAddMethod == "add") { ConfigDefaults.appendProperties(objValue, objAddTo, true); } else if (strAddMethod == "head") { //strangely, you can't use Array(objAddTo[objIndex]["splice"])(0,0,objValue); objAddTo[objIndex]["splice"](0,0,objValue); } else if (strAddMethod == "tail") { objAddTo[objIndex]["push"](objValue); } } //we're not using backward-compatible methods else { if (booSpliceValue) { objAddTo["splice"](Number(objIndex),0,objValue); } else { objAddTo[ objIndex ] = objValue; } } } //we're not at the end of the path, so recurse else { objAddTo = objAddTo[objIndex]; usePathToSetValue(arrRefinedSteps, objValue, objAddTo, objParent, strAddMethod, objIndex); } } /*In some cases, containers (path parts) must be prepped via new Object() or * new Array(). In the case of this object and this path, no prep is needed: * * object: {first: "Bill" last: "Jones"} * path: "last" * value: "Smith" * * Since last already exists, "Smith" can replace "Jones." But in the following * case, we need to create the Object workInfo and the Array email. * * object: {first: "Bill" last: "Jones"} * path: "workInfo.email[0]" * value: "bjones@xyz.com" * * If there are more path parts, then we'll be storing them (or rather the next one) * in the container. If there aren't, we'll be storing the value. * * More path parts: if the next part is a text index, we need to make parent[index] a * new Object. Otherwise, we need to make it a new Array. * */ private static function prepContainer(objContainer:Object, arrNextPartsOfPath:Array, objParent:Object, objIndex:Object, booSpliceValue: Boolean) : Void { var objPart:Object; //the container doesn't exist, so we have to create it if (objContainer == undefined) { //true = next path part will be stored in container if (arrNextPartsOfPath.length > 0) { objPart = arrNextPartsOfPath[0]; if (typeof(objPart) == "string") { objParent[ objIndex ] = new Object(); } else { objParent[ objIndex ] = new Array(); } } } //objContainer is defined else { if (booSpliceValue) { //next path part will be spliced into container if (arrNextPartsOfPath.length > 0) { objPart = arrNextPartsOfPath[0]; if (typeof(objPart) == "string") { objParent["splice"](Number(objIndex),0,new Object()); } else { objParent["splice"](Number(objIndex),0,new Array()); } } } } } //recurses over ["friends","kids", 1, "last"], each time delving deeper //into objSearchIn private static function usePathToGetValue(arrRefinedSteps:Array, objSearchIn:Object) : Object { var objFirstItem:Object; var objResult:Object; objFirstItem = arrRefinedSteps.shift(); objResult = objSearchIn[objFirstItem]; if (arrRefinedSteps.length > 0) { return usePathToGetValue(arrRefinedSteps, objSearchIn[objFirstItem]); } return objResult; } private static function findSteps(arrGrossSteps:Array) : Array { var strGrossStep:String; var arrSplitByOpenBracket:Array; var arrExtractedElements:Array; var arrRefinedSteps:Array = new Array(); for(var i:Number = 0; i < arrGrossSteps.length; i++) { strGrossStep = arrGrossSteps[i]; //i = 1: ["friends"] //i = 2: ["kids","1]"] //i = 3: ["last"] arrSplitByOpenBracket = strGrossStep.split("["); //i = 1: ["friends"] //i = 2: ["kids", 1] //i = 3: ["last"] arrExtractedElements = extractElements(arrSplitByOpenBracket); arrRefinedSteps = arrRefinedSteps.concat(arrExtractedElements); } return arrRefinedSteps; } private static function extractElements(arrSplitByOpenBracket:Array) : Array { var strItem:String; var objItemPostSplit:Object; var arrSplitByCloseBracket:Array; var arrResults:Array = new Array(); var arrNumberAndCommand:Array; var numBase:Number; for (var i:Number = 0; i < arrSplitByOpenBracket.length; i++) { //i = 1: "kids" //i = 2: "1]" strItem = arrSplitByOpenBracket[i]; //i = 1: ["kids"] //i = 2: ["1", ""] arrSplitByCloseBracket = strItem.split("]"); //i = 1: "kids" //i = 2: "1" objItemPostSplit = arrSplitByCloseBracket[0]; //i = 1: "kids" //i = 2: 1 if (arrSplitByCloseBracket.length == 2) { arrNumberAndCommand = String(objItemPostSplit).split("#"); if (arrNumberAndCommand.length == 2) { numBase = Number(arrNumberAndCommand[0]); if (numBase < 0) { objItemPostSplit = numBase + -PathEval.NUMBER_FLAG; } else { objItemPostSplit = numBase + PathEval.NUMBER_FLAG; } } else { objItemPostSplit = Number(objItemPostSplit); } } arrResults.push(objItemPostSplit); } return arrResults; } }