/**
* Copyright (c) 2017, Lucee Assosication Switzerland
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
package lucee.runtime.type.scope.storage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lucee.print;
import lucee.commons.collection.MapPro;
import lucee.commons.collection.concurrent.ConcurrentHashMapPro;
import lucee.commons.io.log.Log;
import lucee.commons.lang.RandomUtil;
import lucee.commons.lang.StringUtil;
import lucee.runtime.PageContext;
import lucee.runtime.config.Config;
import lucee.runtime.dump.DumpData;
import lucee.runtime.dump.DumpProperties;
import lucee.runtime.engine.ThreadLocalPageContext;
import lucee.runtime.exp.ExpressionException;
import lucee.runtime.exp.PageException;
import lucee.runtime.listener.ApplicationContext;
import lucee.runtime.op.Caster;
import lucee.runtime.op.Duplicator;
import lucee.runtime.type.Collection;
import lucee.runtime.type.dt.DateTime;
import lucee.runtime.type.dt.DateTimeImpl;
import lucee.runtime.type.it.EntryIterator;
import lucee.runtime.type.it.ValueIterator;
import lucee.runtime.type.scope.Scope;
import lucee.runtime.type.scope.Session;
import lucee.runtime.type.scope.session.IKStorageScopeSession;
import lucee.runtime.type.scope.client.IKStorageScopeClient;
import lucee.runtime.type.util.CollectionUtil;
import lucee.runtime.type.util.KeyConstants;
import lucee.runtime.type.util.StructSupport;
import lucee.runtime.type.util.StructUtil;
public abstract class IKStorageScopeSupport extends StructSupport implements StorageScope {
//public static int STORAGE_TYPE_DATASOURCE=1;
//public static int STORAGE_TYPE_CACHE=2;
public static Collection.Key CFID=KeyConstants._cfid;
public static Collection.Key CFTOKEN=KeyConstants._cftoken;
public static Collection.Key URLTOKEN=KeyConstants._urltoken;
public static Collection.Key LASTVISIT=KeyConstants._lastvisit;
public static Collection.Key HITCOUNT=KeyConstants._hitcount;
public static Collection.Key TIMECREATED=KeyConstants._timecreated;
public static Collection.Key SESSION_ID=KeyConstants._sessionid;
protected static final IKStorageScopeItem ONE = new IKStorageScopeItem("1");
private static int _id=0;
private int id=0;
private static final long serialVersionUID = 7874930250042576053L;
private static final IKStorageScopeItem NULL = new IKStorageScopeItem("null");
private static Set<Collection.Key> FIX_KEYS=new HashSet<Collection.Key>();
static {
FIX_KEYS.add(CFID);
FIX_KEYS.add(CFTOKEN);
FIX_KEYS.add(URLTOKEN);
FIX_KEYS.add(LASTVISIT);
FIX_KEYS.add(HITCOUNT);
FIX_KEYS.add(TIMECREATED);
}
protected static Set<Collection.Key> ignoreSet=new HashSet<Collection.Key>();
static {
ignoreSet.add(CFID);
ignoreSet.add(CFTOKEN);
ignoreSet.add(URLTOKEN);
}
protected boolean isinit=true;
protected MapPro<Collection.Key,IKStorageScopeItem> data;
protected long lastvisit;
protected DateTime _lastvisit;
protected int hitcount=0;
protected DateTime timecreated;
private boolean hasChanges=false;
protected String strType;
protected int type;
private long timeSpan=-1;
private String storage;
private Map<String, String> tokens;
private long lastModified;
private IKHandler handler;
private String appName;
private String name;
private String cfid;
public IKStorageScopeSupport(PageContext pc, IKHandler handler, String appName,String name,String strType,int type,MapPro<Collection.Key,IKStorageScopeItem> data, long lastModified) {
// !!! do not store the pagecontext or config object, this object is Serializable !!!
Config config = ThreadLocalPageContext.getConfig(pc);
this.data=data;
timecreated=doNowIfNull(config,Caster.toDate(data.g(TIMECREATED,null),false,pc.getTimeZone(),null));
_lastvisit=doNowIfNull(config,Caster.toDate(data.g(LASTVISIT,null),false,pc.getTimeZone(),null));
if(_lastvisit==null) _lastvisit=timecreated;
lastvisit=_lastvisit==null?0:_lastvisit.getTime();
this.hitcount=(type==SCOPE_CLIENT)?Caster.toIntValue(data.g(HITCOUNT,ONE),1):1;
this.strType=strType;
this.type=type;
this.lastModified=lastModified;
this.handler=handler;
this.appName=appName;
this.name=name;
this.cfid=pc.getCFID();
id=++_id;
}
/**
* Constructor of the class
* @param other
* @param deepCopy
*/
protected IKStorageScopeSupport(IKStorageScopeSupport other, boolean deepCopy) {
this.data=(MapPro<Collection.Key, IKStorageScopeItem>)
Duplicator.duplicateMap(other.data, new ConcurrentHashMapPro<Collection.Key, IKStorageScopeItem>(), deepCopy);
this.timecreated=other.timecreated;
this._lastvisit=other._lastvisit;
this.hitcount=other.hitcount;
this.isinit=other.isinit;
this.lastvisit=other.lastvisit;
this.strType=other.strType;
this.type=other.type;
this.timeSpan=other.timeSpan;
id=++_id;
this.lastModified=other.lastModified;
this.handler=other.handler;
this.appName=other.appName;
this.name=other.name;
this.cfid=other.cfid;
}
public synchronized static Scope getInstance(int scope, IKHandler handler, String appName, String name, PageContext pc, Scope existing, Log log) throws PageException {
IKStorageValue sv=null;
if(Scope.SCOPE_SESSION==scope) sv= handler.loadData(pc, appName,name, "session",Scope.SCOPE_SESSION, log);
else if(Scope.SCOPE_CLIENT==scope) sv= handler.loadData(pc, appName,name, "client",Scope.SCOPE_CLIENT, log);
if(sv!=null) {
long time = sv.lastModified();
if(existing instanceof IKStorageScopeSupport) {
if(((IKStorageScopeSupport)existing).lastModified()>=time) {
return existing;
}
}
if(Scope.SCOPE_SESSION==scope) return new IKStorageScopeSession(pc,handler,appName,name,sv.getValue(),time);
else if(Scope.SCOPE_CLIENT==scope) return new IKStorageScopeClient(pc,handler,appName,name,sv.getValue(),time);
}
else if(existing!=null) {
return existing;
}
IKStorageScopeSupport rtn=null;
ConcurrentHashMapPro<Key, IKStorageScopeItem> map = new ConcurrentHashMapPro<Collection.Key,IKStorageScopeItem>();
if(Scope.SCOPE_SESSION==scope) rtn= new IKStorageScopeSession(pc,handler,appName,name,map,0);
else if(Scope.SCOPE_CLIENT==scope) rtn= new IKStorageScopeClient(pc,handler,appName,name,map,0);
rtn.store(pc);
return rtn;
}
public static Scope getInstance(int scope, IKHandler handler, String appName, String name, PageContext pc, Session existing, Log log, Session defaultValue) {
try {
return getInstance(scope, handler, appName, name, pc,existing, log);
}
catch (PageException e) {}
return defaultValue;
}
public synchronized static boolean hasInstance(int scope, IKHandler handler, String appName, String name, PageContext pc) {
try {
if(Scope.SCOPE_SESSION==scope) return handler.loadData(pc, appName,name, "session",Scope.SCOPE_SESSION, null)!=null;
else if(Scope.SCOPE_CLIENT==scope) return handler.loadData(pc, appName,name, "client",Scope.SCOPE_CLIENT, null)!=null;
return false;
}
catch (PageException e) {
return false;
}
}
@Override
public void touchBeforeRequest(PageContext pc) {
hasChanges=false;
setTimeSpan(pc);
//lastvisit=System.currentTimeMillis();
if(data==null) data=new ConcurrentHashMapPro<Collection.Key, IKStorageScopeItem>();
data.put(KeyConstants._cfid, new IKStorageScopeItem(pc.getCFID()));
data.put(KeyConstants._cftoken, new IKStorageScopeItem(pc.getCFToken()));
data.put(URLTOKEN, new IKStorageScopeItem(pc.getURLToken()));
data.put(LASTVISIT, new IKStorageScopeItem(_lastvisit));
_lastvisit=new DateTimeImpl(pc.getConfig());
lastvisit=System.currentTimeMillis();
if(type==SCOPE_CLIENT){
data.put(HITCOUNT, new IKStorageScopeItem(new Double(hitcount++)));
}
else {
data.put(SESSION_ID, new IKStorageScopeItem(pc.getApplicationContext().getName()+"_"+pc.getCFID()+"_"+pc.getCFToken()));
}
data.put(TIMECREATED, new IKStorageScopeItem(timecreated));
}
public void resetEnv(PageContext pc){
_lastvisit=new DateTimeImpl(pc.getConfig());
timecreated=new DateTimeImpl(pc.getConfig());
touchBeforeRequest(pc);
}
void setTimeSpan(PageContext pc) {
ApplicationContext ac=pc.getApplicationContext();
this.timeSpan=getType()==SCOPE_SESSION?
ac.getSessionTimeout().getMillis():
ac.getClientTimeout().getMillis();
}
@Override
public void setMaxInactiveInterval(int interval) {
this.timeSpan=interval*1000L;
}
@Override
public int getMaxInactiveInterval() {
return (int)(this.timeSpan/1000L);
}
@Override
public final boolean isInitalized() {
return isinit;
}
public long lastModified() {
return lastModified;
}
@Override
public final void initialize(PageContext pc) {
// StorageScopes need only request initialisation no global init, they are not reused;
}
@Override
public void touchAfterRequest(PageContext pc) {
setTimeSpan(pc);
data.put(LASTVISIT, new IKStorageScopeItem(_lastvisit));
data.put(TIMECREATED, new IKStorageScopeItem(timecreated));
if(type==SCOPE_CLIENT){
data.put(HITCOUNT, new IKStorageScopeItem(new Double(hitcount)));
}
store(pc);
}
@Override
public final void release(PageContext pc) {
clear();
isinit=false;
}
/**
* @return returns if the scope is empty or not, this method ignore the "constant" entries of the scope (cfid,cftoken,urltoken)
*/
public boolean hasContent() {
if(data.size()==(type==SCOPE_CLIENT?6:5) && data.containsKey(URLTOKEN) && data.containsKey(KeyConstants._cftoken) && data.containsKey(KeyConstants._cfid)) {
return false;
}
return true;
}
@Override
public void clear() {
data.clear();
}
@Override
public boolean containsKey(Key key) {
return data.containsKey(key);
}
@Override
public Object get(Key key) throws PageException {
return data.g(key).getValue();
}
@Override
public Object get(Key key, Object defaultValue) {
IKStorageScopeItem v = data.g(key, NULL);
if(v==NULL) return defaultValue;
return v.getValue();
}
@Override
public Iterator<Collection.Key> keyIterator() {
return data.keySet().iterator();
}
@Override
public Iterator<Entry<Key, Object>> entryIterator() {
return new EntryIterator(this, keys());
}
@Override
public Iterator<Object> valueIterator() {
return new ValueIterator(this, keys()); // TODO use or make a faster iterator
}
@Override
public lucee.runtime.type.Collection.Key[] keys() {
return CollectionUtil.keys(this);
}
@Override
public Object remove(Key key) throws PageException {
hasChanges=true;
IKStorageScopeItem existing = data.get(key);
if(existing!=null) {
return existing.remove();
}
throw new ExpressionException("can't remove key ["+key.getString()+"] from map, key doesn't exist");
}
@Override
public Object removeEL(Key key) {
hasChanges=true;
IKStorageScopeItem existing = data.get(key);
if(existing!=null) {
return existing.remove();
}
return null;
}
@Override
public Object set(Key key, Object value) throws PageException {
hasChanges=true;
return data.put(key, new IKStorageScopeItem(value));
}
@Override
public Object setEL(Key key, Object value) {
hasChanges=true;
return data.put(key, new IKStorageScopeItem(value));
}
@Override
public long lastVisit() {
return lastvisit;
}
public Collection.Key[] pureKeys() {
List<Collection.Key> keys=new ArrayList<Collection.Key>();
Iterator<Key> it = keyIterator();
Collection.Key key;
while(it.hasNext()){
key=it.next();
if(!FIX_KEYS.contains(key))keys.add(key);
}
return keys.toArray(new Collection.Key[keys.size()]);
}
@Override
public int size() {
return data.size();
}
public void store(PageContext pc){ // FUTURE add to interface
handler.store(this, pc, appName, name, cfid, data,ThreadLocalPageContext.getConfig(pc).getLog("scope"));
}
public void unstore(PageContext pc){
handler.unstore(this, pc, appName, name, cfid,ThreadLocalPageContext.getConfig(pc).getLog("scope"));
}
public void store(Config config){
store(ThreadLocalPageContext.get());
}
public void unstore(Config config){
unstore(ThreadLocalPageContext.get());
}
/**
* @return the hasChanges
*/
public boolean hasChanges() {
return hasChanges;
}
@Override
public boolean containsValue(Object value) {
return data.containsValue(value);
}
@Override
public java.util.Collection values() {
java.util.Collection<Object> res=new ArrayList<Object>();
Iterator<IKStorageScopeItem> it = data.values().iterator();
while(it.hasNext()) {
res.add(it.next().getValue());
}
return res;
}
@Override
public final int getType() {
return type;
}
@Override
public final String getTypeAsString() {
return strType;
}
@Override
public final DumpData toDumpData(PageContext pageContext, int maxlevel, DumpProperties dp) {
return StructUtil.toDumpTable(this, StringUtil.ucFirst(getTypeAsString())+" Scope (IK "+getStorageType()+")", pageContext, maxlevel, dp);
}
@Override
public long getLastAccess() { return lastvisit;}
@Override
public long getTimeSpan() { return timeSpan;}
@Override
public void touch() {
lastvisit=System.currentTimeMillis();
_lastvisit=new DateTimeImpl(ThreadLocalPageContext.getConfig());
}
@Override
public boolean isExpired() {
return (getLastAccess()+getTimeSpan())<System.currentTimeMillis();
}
@Override
public void setStorage(String storage) {
this.storage=storage;
}
@Override
public String getStorage() {
return storage;
}
public static String encode(String input) {
int len=input.length();
StringBuilder sb=new StringBuilder();
char c;
for(int i=0;i<len;i++){
c=input.charAt(i);
if((c>='0' && c<='9') || (c>='a' && c<='z') || (c>='A' && c<='Z') || c=='_' || c=='-')
sb.append(c);
else {
sb.append('$');
sb.append(Integer.toString((c),Character.MAX_RADIX));
sb.append('$');
}
}
return sb.toString();
}
public static String decode(String input) {
int len=input.length();
StringBuilder sb=new StringBuilder();
char c;
int ni;
for(int i=0;i<len;i++){
c=input.charAt(i);
if(c=='$') {
ni=input.indexOf('$',i+1);
sb.append((char)Integer.parseInt(input.substring(i+1,ni),Character.MAX_RADIX));
i=ni;
}
else {
sb.append(c);
}
}
return sb.toString();
}
public int _getId() {
return id;
}
@Override
public long getCreated() {
return timecreated==null?0:timecreated.getTime();
}
@Override
public synchronized String generateToken(String key, boolean forceNew) {
if(tokens==null)
tokens = new HashMap<String,String>();
// get existing
String token;
if(!forceNew) {
token = tokens.get(key);
if(token!=null) return token;
}
// create new one
token = RandomUtil.createRandomStringLC(40);
tokens.put(key, token);
return token;
}
@Override
public synchronized boolean verifyToken(String token, String key) {
if(tokens==null) return false;
String _token = tokens.get(key);
return _token!=null && _token.equalsIgnoreCase(token);
}
public static void merge(MapPro<Key, IKStorageScopeItem> local,MapPro<Key, IKStorageScopeItem> storage) {
Iterator<Entry<Key, IKStorageScopeItem>> it = local.entrySet().iterator();
Entry<Key, IKStorageScopeItem> e;
IKStorageScopeItem storageItem;
while(it.hasNext()) {
e = it.next();
storageItem = storage.get(e.getKey());
// this entry not exist in the storage
if(storageItem==null) {
if(!e.getValue().removed()) storage.put(e.getKey(), e.getValue());
}
// local is newer than storage
else if(e.getValue().lastModified()>storageItem.lastModified()) {
if(e.getValue().removed()) storage.remove(e.getKey());
else {
storage.put(e.getKey(), e.getValue());
}
}
// local is older than storage is ignored?
}
}
public static MapPro<Key, IKStorageScopeItem> cleanRemoved(MapPro<Key, IKStorageScopeItem> local) {
Iterator<Entry<Key, IKStorageScopeItem>> it = local.entrySet().iterator();
Entry<Key, IKStorageScopeItem> e;
while(it.hasNext()) {
e=it.next();
if(e.getValue().removed()) local.remove(e.getKey());
}
return local;
}
public static MapPro<Key, IKStorageScopeItem> prepareToStore(MapPro<Key, IKStorageScopeItem> local,
Object oStorage,long lastModified) throws PageException {
// cached data changed in meantime
if(oStorage instanceof IKStorageValue) {
IKStorageValue storage=(IKStorageValue) oStorage;
if(storage.lastModified()>lastModified) {
MapPro<Key, IKStorageScopeItem> trg = storage.getValue();
IKStorageScopeSupport.merge(local,trg);
return trg;
}
else {
return IKStorageScopeSupport.cleanRemoved(local);
}
}
return local;
}
@Override
public final String getStorageType() {
return handler.getType();
}
protected static DateTime doNowIfNull(Config config,DateTime dt) {
if(dt==null)return new DateTimeImpl(config);
return dt;
}
//protected abstract IKStorageValue loadData(PageContext pc, String appName, String name,String strType,int type, Log log) throws PageException;
}