/**
* Copyright 2008-2016 Qualogy Solutions B.V.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qualogy.qafe.core.datastore;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.map.CaseInsensitiveMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import com.qualogy.qafe.bind.commons.type.Parameter;
/**
* Element to be inserted in datastore. This class wraps a Map (CaseInsensitiveMap in this matter)
* and so supports key, value get and add (put).
*
* The CaseInsensitiveMap allows the data in this class to be searched case insensitive, so for example:
*
* Map map = new CaseInsensitiveMap();
* map.put("One", "a");
* map.put("Two", "b");
* map.put("one", "c");
* map.put(null, "d");//will throw an illegalargumentexception since key cannot be null
*
* - creates a CaseInsensitiveMap with two entries
* - map.get(null) will throw an illegalargumentexception since key cannot be null
* - map.get("ONE") returns "c".
* - the Set returned by keySet() equals {"one", "two"}.
*
* NOTE: the CaseInsensitiveMap has one disadvantage, being that the keys are converted before storage
*
* LastTouched (for cleanup purposes) is updated after the following actions on this object:
* - add
* - get
* - create (in constructor)
*
* @author
*/
@SuppressWarnings("unchecked") //generic class
public class Data {
private long lastTouched;
private Map elements;
public Map getElements() {
return elements;
}
private Data(){
//elements = new CaseInsensitiveMap();
elements = new DataMap();
updateLastTouched();
}
protected static Data create(){
return new Data();
}
protected void clear(){
elements.clear();
}
/**
* lastTouched is updated after get
* @param key
* @return
*/
protected final Object get(String key) throws DataNotFoundException {
updateKey(key);
Object result = elements;
try{
if(key.indexOf(Parameter.OBJECT_DELIMITER)>-1){//cheaper to check fst before creating tokenizer
result = getNested(result, key);
}else{
result = getResult(result, key);
}
}catch(DataNotFoundException e){
throw new DataNotFoundException("Data cannot be found for ["+key+"]", e);
}
updateLastTouched();
return result;
}
protected final Object find(String key){
Object result = null;
try{
result = get(key);
}catch(DataNotFoundException e){
//nothing since it is a find
}
return result;
}
/**
* @pre key is not null
* @pre key is to lower case
* method returns null when no data found and throws keynotfoundException when key has not been registered
* @param from
* @param key
* @return
*/
private Object getResult(Object from, String key) throws DataNotFoundException{
Object result = null;
if(from!=null){
if(!(from instanceof Map))
throw new IllegalArgumentException("Trying to get nested result from a non-nested object. Key that was used: "+key+" on object:"+from+"("+from.getClass()+")");
boolean isList = key.indexOf("[")>-1;
String getKey = (isList)?key.substring(0, key.indexOf("[")):key;
if(!((Map)from).containsKey(getKey)&& !getKey.contains(""+Parameter.ATTRIBUTE_DELIMITER))
throw new DataNotFoundException("Data cannot be found for endkey ["+key+"]");
result = ((Map)from).get(getKey);
if(isList && result instanceof List){
int i = Integer.parseInt(key.substring(key.indexOf("[")+1,key.indexOf("]")));
result = ((List)result).get(i);
} else {
if (key.contains(""+Parameter.ATTRIBUTE_DELIMITER)){
String attribute = key.substring(key.indexOf(Parameter.ATTRIBUTE_DELIMITER)+1,key.length());
String elementKey = key.substring(0,key.indexOf(Parameter.ATTRIBUTE_DELIMITER));
Object object = elements.get(elementKey);
if (object instanceof Collection){
if (Parameter.ATTRIBUTE_SIZE.equals(attribute)){
result = ((Collection)object).size();
}
}
}
}
}
/* Commented coz when only one value is selected in listbox,
* its converted to hashmap instead of keeping the type as list itself
*
* if(result instanceof List && ((List)result).size()==1){
result = ((List)result).get(0);
}*/
return result;
}
/**
* @pre key is not null
* @pre key is to lower case
* @param result
* @param key
* @return
*/
private Object getNested(Object result, String key) throws DataNotFoundException{
String[] tokens = StringUtils.split(key, Parameter.OBJECT_DELIMITER);
for (int i = 0; i < tokens.length; i++) {
result = getResult(result, tokens[i]);
}
return result;
}
protected boolean contains(String key){
//TODO:come on!!!
try{
get(key);
return true;
}catch(DataNotFoundException e){
return false;
}
}
/**
* method to add data to this
* lastTouched is updated after add
* @param key
* @param value
*/
protected final void add(String key, Object value) {
key = updateKey(key);
String[] tokens = StringUtils.split(key, Parameter.OBJECT_DELIMITER);
Object mother = elements;
for (int i = 0; i < tokens.length; i++) {
if(i == tokens.length-1){//last token
String partkey = tokens[i];
if(tokens[i].indexOf("[")>-1){
partkey = tokens[i].substring(0, tokens[i].indexOf("["));
List tmpList = (List)((Map)mother).get(partkey);
if(tmpList==null)tmpList = new ArrayList();
value = addToList(tokens[i], tmpList, value);
}
((Map)mother).put(partkey, value);
break;
}
//if more tokens, find next object in the map
//(assumption for map is because if there are tokens left, token search can only be on a map)
mother = getNextObj((Map)mother, tokens[i]);
}
updateLastTouched();
}
/**
* @pre within the list there can be no more map, assuming the last token is past on to this method
* @param token
* @param mother
* @param value
*/
private List addToList(String token, List mother, Object value) {
int indexOfLastStartingBracket = token.indexOf("[");
int indexOfLastClosingBracket = token.indexOf("]");
if(indexOfLastClosingBracket<0)
throw new IllegalArgumentException("specified token ["+token+"], but object for this key is a list, so index is needed");
while(indexOfLastClosingBracket>-1){//in case of f.i. key[0][0].key
String indexStr = token.substring(indexOfLastStartingBracket+1, indexOfLastClosingBracket);
if(!NumberUtils.isNumber(indexStr))
throw new IllegalArgumentException("index on data, set by [<index>], must be a number, found ["+indexStr+"]");
indexOfLastStartingBracket = token.indexOf("[", indexOfLastClosingBracket+1);
indexOfLastClosingBracket = token.indexOf("]", indexOfLastClosingBracket+1);
if(indexOfLastClosingBracket<0){//if this is the last, add the value
mother.add(Integer.parseInt(indexStr),value);
}else{
mother = (List)mother.get(Integer.parseInt(indexStr));
}
}
return mother;
}
/**
* @pre this is not the deepest nested object in line
* @param mother
* @param token
*/
private Object getNextObj(Map<String, Object> mother, String token){
Object result = null;
boolean isList = token.indexOf("[")>-1;//according to the user this is a list
String key = isList?token.substring(0, token.indexOf("[")):token;//get root key, without index
if(!isList && !(mother.get(key) instanceof Map)){//create the map if not exists
mother.put(key, new HashMap<String,Object>());
}else if(isList && !(mother.get(key) instanceof List)){//create the list if not exists
mother.put(key, new ArrayList<Object>());
}
if(isList){
Object tmpresult = (List)mother.get(key);
int indexOfLastStartingBracket = token.indexOf("[");
int indexOfLastClosingBracket = token.indexOf("]");
while(indexOfLastClosingBracket>-1){//in case of f.i. key[0][0].key
String indexStr = token.substring(indexOfLastStartingBracket+1, indexOfLastClosingBracket);
indexOfLastStartingBracket = token.indexOf("[", indexOfLastClosingBracket+1);
indexOfLastClosingBracket = token.indexOf("]", indexOfLastClosingBracket+1);
if(!NumberUtils.isNumber(indexStr))
throw new IllegalArgumentException("index on data, set by [<index>], must be a number, found ["+indexStr+"]");
int listSize = ((List)tmpresult).size();
if(listSize==Integer.parseInt(indexStr)){
if(indexOfLastClosingBracket>0){
((List)tmpresult).add(new ArrayList());
}else{
((List)tmpresult).add(new HashMap());
}
}else if(Integer.parseInt(indexStr)>listSize){
throw new IllegalArgumentException("trying to add data on position ["+indexStr+"] in a list of size ["+listSize+"]");
}
tmpresult = ((List)tmpresult).get(Integer.parseInt(indexStr));
}
result = tmpresult;
}else{
result = mother.get(key);
}
return result;
}
/**
* method validates key and updates it to lowercase
* @param key
*/
private String updateKey(String key){
if(key==null)
throw new IllegalArgumentException("Cannot store data for null key");
if(key.length()==0)
throw new IllegalArgumentException("Cannot store data for empty key");
if(key.endsWith(""+Parameter.OBJECT_DELIMITER))
throw new IllegalArgumentException("Cannot end key with "+Parameter.OBJECT_DELIMITER);
return key;
}
protected boolean isTouchedAfter(long time) {
return lastTouched>time;
}
private void updateLastTouched(){
lastTouched = Calendar.getInstance().getTime().getTime();
}
protected String toLogString(){
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(lastTouched);
StringBuilder builder = new StringBuilder(255);
builder.append("Last touched > " + cal.getTime() + (!elements.isEmpty()?"\n":""));
DataToLogStringBuilder.build(elements,builder);
return builder.toString();
}
}