Mercurial Hosting > luan
view src/org/eclipse/jetty/server/session/JDBCSessionManager.java @ 807:947b11aa3157
remove jetty favicon
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Wed, 07 Sep 2016 23:26:12 -0600 |
parents | 3428c60d7cfc |
children |
line wrap: on
line source
// // ======================================================================== // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.server.session; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import javax.servlet.SessionTrackingMode; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import org.eclipse.jetty.server.SessionIdManager; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** * JDBCSessionManager * * SessionManager that persists sessions to a database to enable clustering. * * Session data is persisted to the JettySessions table: * * rowId (unique in cluster: webapp name/path + virtualhost + sessionId) * contextPath (of the context owning the session) * sessionId (unique in a context) * lastNode (name of node last handled session) * accessTime (time in milliseconds session was accessed) * lastAccessTime (previous time in milliseconds session was accessed) * createTime (time in milliseconds session created) * cookieTime (time in milliseconds session cookie created) * lastSavedTime (last time in milliseconds session access times were saved) * expiryTime (time in milliseconds that the session is due to expire) * map (attribute map) * * As an optimization, to prevent thrashing the database, we do not persist * the accessTime and lastAccessTime every time the session is accessed. Rather, * we write it out every so often. The frequency is controlled by the saveIntervalSec * field. */ public class JDBCSessionManager extends AbstractSessionManager { private static final Logger LOG = Log.getLogger(JDBCSessionManager.class); private ConcurrentHashMap<String, AbstractSession> _sessions; protected JDBCSessionIdManager _jdbcSessionIdMgr = null; protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs /** * Session * * Session instance. */ public class Session extends AbstractSession { private static final long serialVersionUID = 5208464051134226143L; /** * If dirty, session needs to be (re)persisted */ private boolean _dirty=false; /** * Time in msec since the epoch that a session cookie was set for this session */ private long _cookieSet; /** * Time in msec since the epoch that the session will expire */ private long _expiryTime; /** * Time in msec since the epoch that the session was last persisted */ private long _lastSaved; /** * Unique identifier of the last node to host the session */ private String _lastNode; /** * Virtual host for context (used to help distinguish 2 sessions with same id on different contexts) */ private String _virtualHost; /** * Unique row in db for session */ private String _rowId; /** * Mangled context name (used to help distinguish 2 sessions with same id on different contexts) */ private String _canonicalContext; /** * Session from a request. * * @param request */ protected Session (HttpServletRequest request) { super(JDBCSessionManager.this,request); int maxInterval=getMaxInactiveInterval(); _expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L)); _virtualHost = JDBCSessionManager.getVirtualHost(_context); _canonicalContext = canonicalize(_context.getContextPath()); _lastNode = getSessionIdManager().getWorkerName(); } /** * Session restored from database * @param sessionId * @param rowId * @param created * @param accessed */ protected Session (String sessionId, String rowId, long created, long accessed) { super(JDBCSessionManager.this, created, accessed, sessionId); _rowId = rowId; } protected synchronized String getRowId() { return _rowId; } protected synchronized void setRowId(String rowId) { _rowId = rowId; } public synchronized void setVirtualHost (String vhost) { _virtualHost=vhost; } public synchronized String getVirtualHost () { return _virtualHost; } public synchronized long getLastSaved () { return _lastSaved; } public synchronized void setLastSaved (long time) { _lastSaved=time; } public synchronized void setExpiryTime (long time) { _expiryTime=time; } public synchronized long getExpiryTime () { return _expiryTime; } public synchronized void setCanonicalContext(String str) { _canonicalContext=str; } public synchronized String getCanonicalContext () { return _canonicalContext; } public void setCookieSet (long ms) { _cookieSet = ms; } public synchronized long getCookieSet () { return _cookieSet; } public synchronized void setLastNode (String node) { _lastNode=node; } public synchronized String getLastNode () { return _lastNode; } @Override public void setAttribute (String name, Object value) { super.setAttribute(name, value); _dirty=true; } @Override public void removeAttribute (String name) { super.removeAttribute(name); _dirty=true; } @Override protected void cookieSet() { _cookieSet = getAccessed(); } /** * Entry to session. * Called by SessionHandler on inbound request and the session already exists in this node's memory. * * @see org.eclipse.jetty.server.session.AbstractSession#access(long) */ @Override protected boolean access(long time) { synchronized (this) { if (super.access(time)) { int maxInterval=getMaxInactiveInterval(); _expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L)); return true; } return false; } } /** * Exit from session * @see org.eclipse.jetty.server.session.AbstractSession#complete() */ @Override protected void complete() { synchronized (this) { super.complete(); try { if (isValid()) { if (_dirty) { //The session attributes have changed, write to the db, ensuring //http passivation/activation listeners called willPassivate(); updateSession(this); didActivate(); } else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L)) { updateSessionAccessTime(this); } } } catch (Exception e) { LOG.warn("Problem persisting changed session data id="+getId(), e); } finally { _dirty=false; } } } @Override protected void timeout() throws IllegalStateException { if (LOG.isDebugEnabled()) LOG.debug("Timing out session id="+getClusterId()); super.timeout(); } @Override public String toString () { return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+ ",created="+getCreationTime()+",accessed="+getAccessed()+ ",lastAccessed="+getLastAccessedTime()+",cookieSet="+_cookieSet+ ",lastSaved="+_lastSaved+",expiry="+_expiryTime; } } /** * ClassLoadingObjectInputStream * * Used to persist the session attribute map */ protected class ClassLoadingObjectInputStream extends ObjectInputStream { public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException { super(in); } public ClassLoadingObjectInputStream () throws IOException { super(); } @Override public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException { try { return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { return super.resolveClass(cl); } } } /** * Set the time in seconds which is the interval between * saving the session access time to the database. * * This is an optimization that prevents the database from * being overloaded when a session is accessed very frequently. * * On session exit, if the session attributes have NOT changed, * the time at which we last saved the accessed * time is compared to the current accessed time. If the interval * is at least saveIntervalSecs, then the access time will be * persisted to the database. * * If any session attribute does change, then the attributes and * the accessed time are persisted. * * @param sec */ public void setSaveInterval (long sec) { _saveIntervalSec=sec; } public long getSaveInterval () { return _saveIntervalSec; } /** * A method that can be implemented in subclasses to support * distributed caching of sessions. This method will be * called whenever the session is written to the database * because the session data has changed. * * This could be used eg with a JMS backplane to notify nodes * that the session has changed and to delete the session from * the node's cache, and re-read it from the database. * @param session */ public void cacheInvalidate (Session session) { } /** * A session has been requested by its id on this node. * * Load the session by id AND context path from the database. * Multiple contexts may share the same session id (due to dispatching) * but they CANNOT share the same contents. * * Check if last node id is my node id, if so, then the session we have * in memory cannot be stale. If another node used the session last, then * we need to refresh from the db. * * NOTE: this method will go to the database, so if you only want to check * for the existence of a Session in memory, use _sessions.get(id) instead. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String) */ @Override public Session getSession(String idInCluster) { Session session = null; Session memSession = (Session)_sessions.get(idInCluster); synchronized (this) { //check if we need to reload the session - //as an optimization, don't reload on every access //to reduce the load on the database. This introduces a window of //possibility that the node may decide that the session is local to it, //when the session has actually been live on another node, and then //re-migrated to this node. This should be an extremely rare occurrence, //as load-balancers are generally well-behaved and consistently send //sessions to the same node, changing only iff that node fails. //Session data = null; long now = System.currentTimeMillis(); if (LOG.isDebugEnabled()) { if (memSession==null) LOG.debug("getSession("+idInCluster+"): not in session map,"+ " now="+now+ " lastSaved="+(memSession==null?0:memSession._lastSaved)+ " interval="+(_saveIntervalSec * 1000L)); else LOG.debug("getSession("+idInCluster+"): in session map, "+ " now="+now+ " lastSaved="+(memSession==null?0:memSession._lastSaved)+ " interval="+(_saveIntervalSec * 1000L)+ " lastNode="+memSession._lastNode+ " thisNode="+getSessionIdManager().getWorkerName()+ " difference="+(now - memSession._lastSaved)); } try { if (memSession==null) { LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db."); session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context)); } else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L)) { LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db."); session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context)); } else { LOG.debug("getSession("+idInCluster+"): session in session map"); session = memSession; } } catch (Exception e) { LOG.warn("Unable to load session "+idInCluster, e); return null; } //If we have a session if (session != null) { //If the session was last used on a different node, or session doesn't exist on this node if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null) { //if session doesn't expire, or has not already expired, update it and put it in this nodes' memory if (session._expiryTime <= 0 || session._expiryTime > now) { if (LOG.isDebugEnabled()) LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName()); session.setLastNode(getSessionIdManager().getWorkerName()); _sessions.put(idInCluster, session); //update in db: if unable to update, session will be scavenged later try { updateSessionNode(session); session.didActivate(); } catch (Exception e) { LOG.warn("Unable to update freshly loaded session "+idInCluster, e); return null; } } else { LOG.debug("getSession ({}): Session has expired", idInCluster); session=null; } } else { //the session loaded from the db and the one in memory are the same, so keep using the one in memory session = memSession; LOG.debug("getSession({}): Session not stale {}", idInCluster,session); } } else { //No session in db with matching id and context path. LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster); } return session; } } /** * Get the number of sessions. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions() */ @Override public int getSessions() { int size = 0; synchronized (this) { size = _sessions.size(); } return size; } /** * Start the session manager. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart() */ @Override public void doStart() throws Exception { if (_sessionIdManager==null) throw new IllegalStateException("No session id manager defined"); _jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager; _sessions = new ConcurrentHashMap<String, AbstractSession>(); super.doStart(); } /** * Stop the session manager. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop() */ @Override public void doStop() throws Exception { _sessions.clear(); _sessions = null; super.doStop(); } @Override protected void invalidateSessions() { //Do nothing - we don't want to remove and //invalidate all the sessions because this //method is called from doStop(), and just //because this context is stopping does not //mean that we should remove the session from //any other nodes } /** * Invalidate a session. * * @param idInCluster */ protected void invalidateSession (String idInCluster) { Session session = null; synchronized (this) { session = (Session)_sessions.get(idInCluster); } if (session != null) { session.invalidate(); } } /** * Delete an existing session, both from the in-memory map and * the database. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String) */ @Override protected boolean removeSession(String idInCluster) { synchronized (this) { Session session = (Session)_sessions.remove(idInCluster); try { if (session != null) deleteSession(session); } catch (Exception e) { LOG.warn("Problem deleting session id="+idInCluster, e); } return session!=null; } } /** * Add a newly created session to our in-memory list for this node and persist it. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession) */ @Override protected void addSession(AbstractSession session) { if (session==null) return; synchronized (this) { _sessions.put(session.getClusterId(), session); } //TODO or delay the store until exit out of session? If we crash before we store it //then session data will be lost. try { synchronized (session) { session.willPassivate(); storeSession(((JDBCSessionManager.Session)session)); session.didActivate(); } } catch (Exception e) { LOG.warn("Unable to store new session id="+session.getId() , e); } } /** * Make a new Session. * * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest) */ @Override protected AbstractSession newSession(HttpServletRequest request) { return new Session(request); } /* ------------------------------------------------------------ */ /** Remove session from manager * @param session The session to remove * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and * {@link SessionIdManager#invalidateAll(String)} should be called. */ @Override public void removeSession(AbstractSession session, boolean invalidate) { // Remove session from context and global maps boolean removed = false; synchronized (this) { //take this session out of the map of sessions for this context if (getSession(session.getClusterId()) != null) { removed = true; removeSession(session.getClusterId()); } } if (removed) { // Remove session from all context and global id maps _sessionIdManager.removeSession(session); if (invalidate) _sessionIdManager.invalidateAll(session.getClusterId()); if (invalidate && !_sessionListeners.isEmpty()) { HttpSessionEvent event=new HttpSessionEvent(session); for (HttpSessionListener l : _sessionListeners) l.sessionDestroyed(event); } if (!invalidate) { session.willPassivate(); } } } /** * Expire any Sessions we have in memory matching the list of * expired Session ids. * * @param sessionIds */ protected void expire (List<?> sessionIds) { //don't attempt to scavenge if we are shutting down if (isStopping() || isStopped()) return; //Remove any sessions we already have in memory that match the ids Thread thread=Thread.currentThread(); ClassLoader old_loader=thread.getContextClassLoader(); ListIterator<?> itor = sessionIds.listIterator(); try { while (itor.hasNext()) { String sessionId = (String)itor.next(); if (LOG.isDebugEnabled()) LOG.debug("Expiring session id "+sessionId); Session session = (Session)_sessions.get(sessionId); if (session != null) { session.timeout(); itor.remove(); } else { if (LOG.isDebugEnabled()) LOG.debug("Unrecognized session id="+sessionId); } } } catch (Throwable t) { LOG.warn("Problem expiring sessions", t); } finally { thread.setContextClassLoader(old_loader); } } /** * Load a session from the database * @param id * @return the session data that was loaded * @throws Exception */ protected Session loadSession (final String id, final String canonicalContextPath, final String vhost) throws Exception { final AtomicReference<Session> _reference = new AtomicReference<Session>(); final AtomicReference<Exception> _exception = new AtomicReference<Exception>(); Runnable load = new Runnable() { @SuppressWarnings("unchecked") public void run() { Session session = null; Connection connection=null; PreparedStatement statement = null; try { connection = getConnection(); statement = _jdbcSessionIdMgr._dbAdaptor.getLoadStatement(connection, id, canonicalContextPath, vhost); ResultSet result = statement.executeQuery(); if (result.next()) { session = new Session(id, result.getString(_jdbcSessionIdMgr._sessionTableRowId), result.getLong("createTime"), result.getLong("accessTime")); session.setCookieSet(result.getLong("cookieTime")); session.setLastAccessedTime(result.getLong("lastAccessTime")); session.setLastNode(result.getString("lastNode")); session.setLastSaved(result.getLong("lastSavedTime")); session.setExpiryTime(result.getLong("expiryTime")); session.setCanonicalContext(result.getString("contextPath")); session.setVirtualHost(result.getString("virtualHost")); InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, "map"); ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream (is); Object o = ois.readObject(); session.addAttributes((Map<String,Object>)o); ois.close(); if (LOG.isDebugEnabled()) LOG.debug("LOADED session "+session); } _reference.set(session); } catch (Exception e) { _exception.set(e); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) { try { connection.close();} catch(Exception e) { LOG.warn(e); } } } } }; if (_context==null) load.run(); else _context.getContextHandler().handle(load); if (_exception.get()!=null) { //if the session could not be restored, take its id out of the pool of currently-in-use //session ids _jdbcSessionIdMgr.removeSession(id); throw _exception.get(); } return _reference.get(); } /** * Insert a session into the database. * * @param data * @throws Exception */ protected void storeSession (Session session) throws Exception { if (session==null) return; //put into the database Connection connection = getConnection(); PreparedStatement statement = null; try { String rowId = calculateRowId(session); long now = System.currentTimeMillis(); connection.setAutoCommit(true); statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession); statement.setString(1, rowId); //rowId statement.setString(2, session.getId()); //session id statement.setString(3, session.getCanonicalContext()); //context path statement.setString(4, session.getVirtualHost()); //first vhost statement.setString(5, getSessionIdManager().getWorkerName());//my node id statement.setLong(6, session.getAccessed());//accessTime statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime statement.setLong(8, session.getCreationTime()); //time created statement.setLong(9, session.getCookieSet());//time cookie was set statement.setLong(10, now); //last saved time statement.setLong(11, session.getExpiryTime()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(session.getAttributeMap()); byte[] bytes = baos.toByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob statement.executeUpdate(); session.setRowId(rowId); //set it on the in-memory data as well as in db session.setLastSaved(now); if (LOG.isDebugEnabled()) LOG.debug("Stored session "+session); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) connection.close(); } } /** * Update data on an existing persisted session. * * @param data the session * @throws Exception */ protected void updateSession (Session data) throws Exception { if (data==null) return; Connection connection = getConnection(); PreparedStatement statement = null; try { long now = System.currentTimeMillis(); connection.setAutoCommit(true); statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession); statement.setString(1, getSessionIdManager().getWorkerName());//my node id statement.setLong(2, data.getAccessed());//accessTime statement.setLong(3, data.getLastAccessedTime()); //lastAccessTime statement.setLong(4, now); //last saved time statement.setLong(5, data.getExpiryTime()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(data.getAttributeMap()); byte[] bytes = baos.toByteArray(); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); statement.setBinaryStream(6, bais, bytes.length);//attribute map as blob statement.setString(7, data.getRowId()); //rowId statement.executeUpdate(); data.setLastSaved(now); if (LOG.isDebugEnabled()) LOG.debug("Updated session "+data); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) connection.close(); } } /** * Update the node on which the session was last seen to be my node. * * @param data the session * @throws Exception */ protected void updateSessionNode (Session data) throws Exception { String nodeId = getSessionIdManager().getWorkerName(); Connection connection = getConnection(); PreparedStatement statement = null; try { connection.setAutoCommit(true); statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode); statement.setString(1, nodeId); statement.setString(2, data.getRowId()); statement.executeUpdate(); statement.close(); if (LOG.isDebugEnabled()) LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) connection.close(); } } /** * Persist the time the session was last accessed. * * @param data the session * @throws Exception */ private void updateSessionAccessTime (Session data) throws Exception { Connection connection = getConnection(); PreparedStatement statement = null; try { long now = System.currentTimeMillis(); connection.setAutoCommit(true); statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime); statement.setString(1, getSessionIdManager().getWorkerName()); statement.setLong(2, data.getAccessed()); statement.setLong(3, data.getLastAccessedTime()); statement.setLong(4, now); statement.setLong(5, data.getExpiryTime()); statement.setString(6, data.getRowId()); statement.executeUpdate(); data.setLastSaved(now); statement.close(); if (LOG.isDebugEnabled()) LOG.debug("Updated access time session id="+data.getId()); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) connection.close(); } } /** * Delete a session from the database. Should only be called * when the session has been invalidated. * * @param data * @throws Exception */ protected void deleteSession (Session data) throws Exception { Connection connection = getConnection(); PreparedStatement statement = null; try { connection.setAutoCommit(true); statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession); statement.setString(1, data.getRowId()); statement.executeUpdate(); if (LOG.isDebugEnabled()) LOG.debug("Deleted Session "+data); } finally { if (statement!=null) { try { statement.close(); } catch(Exception e) { LOG.warn(e); } } if (connection!=null) connection.close(); } } /** * Get a connection from the driver. * @return * @throws SQLException */ private Connection getConnection () throws SQLException { return ((JDBCSessionIdManager)getSessionIdManager()).getConnection(); } /** * Calculate a unique id for this session across the cluster. * * Unique id is composed of: contextpath_virtualhost0_sessionid * @param data * @return */ private String calculateRowId (Session data) { String rowId = canonicalize(_context.getContextPath()); rowId = rowId + "_" + getVirtualHost(_context); rowId = rowId+"_"+data.getId(); return rowId; } /** * Get the first virtual host for the context. * * Used to help identify the exact session/contextPath. * * @return 0.0.0.0 if no virtual host is defined */ private static String getVirtualHost (ContextHandler.Context context) { String vhost = "0.0.0.0"; if (context==null) return vhost; String [] vhosts = context.getContextHandler().getVirtualHosts(); if (vhosts==null || vhosts.length==0 || vhosts[0]==null) return vhost; return vhosts[0]; } /** * Make an acceptable file name from a context path. * * @param path * @return */ private static String canonicalize (String path) { if (path==null) return ""; return path.replace('/', '_').replace('.','_').replace('\\','_'); } }