/* * Copyright (c) 2002-2012 Alibaba Group Holding Limited. * All rights reserved. * * 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.alibaba.citrus.service.requestcontext.session.impl; import static com.alibaba.citrus.util.ArrayUtil.*; import static com.alibaba.citrus.util.Assert.*; import static com.alibaba.citrus.util.CollectionUtil.*; import static com.alibaba.citrus.util.ExceptionUtil.*; import static com.alibaba.citrus.util.ObjectUtil.*; import static com.alibaba.citrus.util.StringUtil.*; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.alibaba.citrus.service.requestcontext.RequestContext; import com.alibaba.citrus.service.requestcontext.session.ExactMatchesOnlySessionStore; import com.alibaba.citrus.service.requestcontext.session.SessionConfig; import com.alibaba.citrus.service.requestcontext.session.SessionConfig.CookieConfig; import com.alibaba.citrus.service.requestcontext.session.SessionConfig.IdConfig; import com.alibaba.citrus.service.requestcontext.session.SessionConfig.StoreMappingsConfig; import com.alibaba.citrus.service.requestcontext.session.SessionConfig.StoresConfig; import com.alibaba.citrus.service.requestcontext.session.SessionConfig.UrlEncodeConfig; import com.alibaba.citrus.service.requestcontext.session.SessionIDGenerator; import com.alibaba.citrus.service.requestcontext.session.SessionInterceptor; import com.alibaba.citrus.service.requestcontext.session.SessionModelEncoder; import com.alibaba.citrus.service.requestcontext.session.SessionRequestContext; import com.alibaba.citrus.service.requestcontext.session.SessionStore; import com.alibaba.citrus.service.requestcontext.session.idgen.uuid.impl.UUIDGenerator; import com.alibaba.citrus.service.requestcontext.support.AbstractRequestContextFactory; import com.alibaba.citrus.util.ToStringBuilder; import com.alibaba.citrus.util.ToStringBuilder.MapBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; /** 用来创建和初始化<code>SessionRequestContext</code>的工厂。 */ public class SessionRequestContextFactoryImpl extends AbstractRequestContextFactory<SessionRequestContext> { private final static Logger log = LoggerFactory.getLogger(SessionRequestContext.class); private final ConfigImpl config = new ConfigImpl(); public SessionConfig getConfig() { return config; } /** 初始化factory。 */ @Override protected void init() throws Exception { config.init(); String storeName = config.getStoreMappings().getStoreNameForAttribute(config.getModelKey()); if (storeName == null) { throw new IllegalArgumentException("No storage configured for session model: key=" + config.getModelKey()); } } /** * 包装一个request context。 * * @param wrappedContext 被包装的<code>RequestContext</code>对象 * @return request context */ public SessionRequestContext getRequestContextWrapper(RequestContext wrappedContext) { return new SessionRequestContextImpl(wrappedContext, config); } /** 本类提供了可扩展的session机制。 */ public String[] getFeatures() { return new String[] { "session" }; } /** * 基于cookie的session机制,会在commit时修改cookie和headers,因而依赖于lazyCommit。 * Session框架由于要读写cookie,因此在parser之后。 */ public FeatureOrder[] featureOrders() { return new FeatureOrder[] { new AfterFeature("parseRequest"), new RequiresFeature("lazyCommitHeaders") }; } @Override protected Object dumpConfiguration() { return config; } // 实现SessionConfig。 @SuppressWarnings("unused") private static class ConfigImpl implements SessionConfig { private final IdConfigImpl id = new IdConfigImpl(); private final StoresConfigImpl stores = new StoresConfigImpl(); private final StoreMappingsConfigImpl storeMappings = new StoreMappingsConfigImpl(); private Integer maxInactiveInterval; private Long forceExpirationPeriod; private String modelKey; private Boolean keepInTouch; private SessionModelEncoder[] sessionModelEncoders; private SessionInterceptor[] sessionInterceptors; public int getMaxInactiveInterval() { return maxInactiveInterval; } public void setMaxInactiveInterval(int maxInactiveInterval) { this.maxInactiveInterval = maxInactiveInterval; } public long getForceExpirationPeriod() { return forceExpirationPeriod; } public void setForceExpirationPeriod(long forceExpirationPeriod) { this.forceExpirationPeriod = forceExpirationPeriod; } public String getModelKey() { return modelKey; } public void setModelKey(String modelKey) { this.modelKey = modelKey; } public boolean isKeepInTouch() { return keepInTouch; } public void setKeepInTouch(boolean keepInTouch) { this.keepInTouch = keepInTouch; } public IdConfig getId() { return id; } public StoresConfig getStores() { return stores; } public StoreMappingsConfig getStoreMappings() { return storeMappings; } public SessionModelEncoder[] getSessionModelEncoders() { return sessionModelEncoders; } public void setSessionModelEncoders(SessionModelEncoder[] sessionModelEncoders) { this.sessionModelEncoders = sessionModelEncoders; } public SessionInterceptor[] getSessionInterceptors() { return sessionInterceptors; } public void setSessionInterceptors(SessionInterceptor[] sessionInterceptors) { this.sessionInterceptors = sessionInterceptors; } private void init() throws Exception { maxInactiveInterval = defaultIfNull(maxInactiveInterval, MAX_INACTIVE_INTERVAL_DEFAULT); forceExpirationPeriod = defaultIfNull(forceExpirationPeriod, FORCE_EXPIRATION_PERIOD_DEFAULT); modelKey = defaultIfEmpty(modelKey, MODEL_KEY_DEFAULT); keepInTouch = defaultIfNull(keepInTouch, KEEP_IN_TOUCH_DEFAULT); id.init(); stores.init(this); storeMappings.init(stores); // 对所有的ExactMatchesOnlySessionStore,设置attribute names。 for (String storeName : stores.getStoreNames()) { SessionStore store = stores.getStore(storeName); if (store instanceof ExactMatchesOnlySessionStore) { String[] exactMatchedAttrNames = storeMappings.getExactMatchedAttributeNames(storeName); if (exactMatchedAttrNames == null) { throw new IllegalArgumentException("Session store " + storeName + " only support exact matches to attribute names"); } ((ExactMatchesOnlySessionStore) store).initAttributeNames(exactMatchedAttrNames); } } if (isEmptyArray(sessionModelEncoders)) { sessionModelEncoders = new SessionModelEncoder[] { new SessionModelEncoderImpl() }; } if (isEmptyArray(sessionInterceptors)) { sessionInterceptors = new SessionInterceptor[0]; } for (SessionInterceptor l : sessionInterceptors) { l.init(this); } } @Override public String toString() { MapBuilder mb = new MapBuilder(); mb.append("maxInactiveInterval", String.format("%,d seconds (%,3.2f hours)", maxInactiveInterval, (double) maxInactiveInterval / 3600)); mb.append("forceExpirationPeriod", String.format("%,d seconds (%,3.2f hours)", forceExpirationPeriod, (double) forceExpirationPeriod / 3600)); mb.append("modelKey", modelKey); mb.append("keepInTouch", keepInTouch); mb.append("idConfig", id); mb.append("stores", stores); mb.append("storeMappings", storeMappings); mb.append("sessionModelEncoders", sessionModelEncoders); return new ToStringBuilder().append("SessionConfig").append(mb).toString(); } } @SuppressWarnings("unused") private static class IdConfigImpl implements IdConfig { private final CookieConfigImpl cookie = new CookieConfigImpl(); private final UrlEncodeConfigImpl urlEncode = new UrlEncodeConfigImpl(); private Boolean cookieEnabled; private Boolean urlEncodeEnabled; private SessionIDGenerator generator; public boolean isCookieEnabled() { return cookieEnabled; } public void setCookieEnabled(boolean cookieEnabled) { this.cookieEnabled = cookieEnabled; } public boolean isUrlEncodeEnabled() { return urlEncodeEnabled; } public void setUrlEncodeEnabled(boolean urlEncodeEnabled) { this.urlEncodeEnabled = urlEncodeEnabled; } public CookieConfig getCookie() { return cookie; } public UrlEncodeConfig getUrlEncode() { return urlEncode; } public SessionIDGenerator getGenerator() { return generator; } public void setGenerator(SessionIDGenerator generator) { this.generator = generator; } private void init() { cookieEnabled = defaultIfNull(cookieEnabled, COOKIE_ENABLED_DEFAULT); urlEncodeEnabled = defaultIfNull(urlEncodeEnabled, URL_ENCODE_ENABLED_DEFAULT); if (generator == null) { generator = new UUIDGenerator(); if (generator instanceof InitializingBean) { try { ((InitializingBean) generator).afterPropertiesSet(); } catch (Exception e) { throw toRuntimeException(e); } } } cookie.init(); urlEncode.init(); } @Override public String toString() { MapBuilder mb = new MapBuilder(); mb.append("cookieEnabled", cookieEnabled); mb.append("urlEncodeEnabled", urlEncodeEnabled); mb.append("cookieConfig", cookie); mb.append("urlEncodeConfig", urlEncode); mb.append("generator", generator); return new ToStringBuilder().append("IdConfig").append(mb).toString(); } } @SuppressWarnings("unused") private static class CookieConfigImpl implements CookieConfig { private String name; private String domain; private String path; private Integer maxAge; private Boolean httpOnly; private Boolean secure; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDomain() { return domain; } public void setDomain(String domain) { // normalize domain domain = trimToNull(domain); if (domain != null && !domain.startsWith(".")) { domain = "." + domain; } this.domain = domain; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public int getMaxAge() { return maxAge; } public void setMaxAge(int maxAge) { this.maxAge = maxAge; } public boolean isHttpOnly() { return httpOnly; } public void setHttpOnly(boolean httpOnly) { this.httpOnly = httpOnly; } public boolean isSecure() { return secure; } public void setSecure(boolean secure) { this.secure = secure; } private void init() { name = defaultIfEmpty(name, COOKIE_NAME_DEFAULT); domain = defaultIfEmpty(domain, COOKIE_DOMAIN_DEFAULT); path = defaultIfEmpty(path, COOKIE_PATH_DEFAULT); maxAge = defaultIfNull(maxAge, COOKIE_MAX_AGE_DEFAULT); httpOnly = defaultIfNull(httpOnly, COOKIE_HTTP_ONLY_DEFAULT); secure = defaultIfNull(secure, COOKIE_SECURE_DEFAULT); } @Override public String toString() { MapBuilder mb = new MapBuilder(); mb.append("name", name); mb.append("domain", domain); mb.append("path", path); mb.append("maxAge", String.format("%,d seconds", maxAge)); mb.append("httpOnly", httpOnly); mb.append("secure", secure); return new ToStringBuilder().append("CookieConfig").append(mb).toString(); } } @SuppressWarnings("unused") private static class UrlEncodeConfigImpl implements UrlEncodeConfig { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } private void init() { name = defaultIfEmpty(name, URL_ENCODE_NAME_DEFAULT); } @Override public String toString() { MapBuilder mb = new MapBuilder(); mb.append("name", name); return new ToStringBuilder().append("UrlEncodeConfig").append(mb).toString(); } } @SuppressWarnings("unused") private static class StoresConfigImpl implements StoresConfig { private Map<String, SessionStore> stores; public void setStores(LinkedHashMap<String, SessionStore> stores) { this.stores = stores; } public SessionStore getStore(String storeName) { return stores.get(storeName); } public String[] getStoreNames() { return stores.keySet().toArray(new String[stores.size()]); } private void init(SessionConfig sessionConfig) throws Exception { if (stores == null) { stores = createLinkedHashMap(); } // 初始化所有stores for (Map.Entry<String, SessionStore> entry : stores.entrySet()) { entry.getValue().init(entry.getKey(), sessionConfig); } } @Override public String toString() { return new ToStringBuilder().append("Stores").append(stores).toString(); } } @SuppressWarnings("unused") private static class StoreMappingsConfigImpl implements StoreMappingsConfig { private AttributePattern[] patterns; private String defaultStore; private Map<String, String> attributeMatchCache; public void setPatterns(AttributePattern[] patterns) { this.patterns = patterns; } private void init(StoresConfig stores) { this.attributeMatchCache = createConcurrentHashMap(); if (patterns == null) { patterns = new AttributePattern[0]; } for (AttributePattern pattern : patterns) { if (pattern.isDefaultPattern()) { if (defaultStore != null) { throw new IllegalArgumentException("More than one stores mapped to *: " + defaultStore + " and " + pattern.getStoreName()); } defaultStore = pattern.getStoreName(); } if (stores.getStore(pattern.getStoreName()) == null) { throw new IllegalArgumentException("Undefined Session Store: " + pattern); } } } public String getStoreNameForAttribute(String attrName) { attrName = assertNotNull(trimToNull(attrName), "attrName"); String matchedStoreName = attributeMatchCache.get(attrName); if (matchedStoreName != null) { return matchedStoreName; } else { List<AttributeMatch> matches = createArrayList(patterns.length); for (AttributePattern pattern : patterns) { if (pattern.isDefaultPattern()) { matches.add(new AttributeMatch(pattern, 0)); } else if (pattern.isRegexPattern()) { Matcher matcher = pattern.getPattern().matcher(attrName); if (matcher.find()) { matches.add(new AttributeMatch(pattern, matcher.end() - matcher.start())); } } else { if (pattern.patternName.equals(attrName)) { matches.add(new AttributeMatch(pattern, pattern.patternName.length())); } } } // 最长匹配优先 Collections.sort(matches); if (log.isTraceEnabled()) { ToStringBuilder buf = new ToStringBuilder(); buf.format("Attribute \"%s\" ", attrName); if (matches.isEmpty()) { buf.append("does not match any pattern"); } else { buf.append("matches the following CANDIDATED patterns:").append(matches); } log.trace(buf.toString()); } if (!matches.isEmpty()) { matchedStoreName = matches.get(0).pattern.getStoreName(); } if (matchedStoreName != null) { attributeMatchCache.put(attrName, matchedStoreName); } } if (log.isDebugEnabled() && matchedStoreName != null) { log.debug("Session attribute {} is handled by session store: {}", attrName, matchedStoreName); } return matchedStoreName; } public String[] getExactMatchedAttributeNames(String storeName) { storeName = assertNotNull(trimToNull(storeName), "no storeName"); Set<String> attrNames = createLinkedHashSet(); for (AttributePattern pattern : patterns) { if (pattern.getStoreName().equals(storeName)) { // 如果是非精确匹配,则返回null。 if (pattern.isDefaultPattern() || pattern.isRegexPattern()) { return null; } attrNames.add(pattern.patternName); } } return attrNames.toArray(new String[attrNames.size()]); } @Override public String toString() { return new ToStringBuilder().append("StoreMappings").append(patterns).toString(); } } /** 代表一个attribute的匹配。 */ private static class AttributeMatch implements Comparable<AttributeMatch> { private final AttributePattern pattern; private final int matchLength; public AttributeMatch(AttributePattern pattern, int matchLength) { this.pattern = pattern; this.matchLength = matchLength; } /** 先比较匹配长度,较长的优先。其次比较匹配的类型,精确匹配比正则表达式匹配优先。 */ public int compareTo(AttributeMatch o) { int result = o.matchLength - matchLength; if (result == 0) { int r1 = pattern.isRegexPattern() ? 0 : 1; int r2 = o.pattern.isRegexPattern() ? 0 : 1; return r2 - r1; } return result; } @Override public String toString() { return new ToStringBuilder().append(pattern).append(", matchLength=").append(matchLength).toString(); } } /** 代表一个pattern的信息。 */ static class AttributePattern { public final String patternName; public final String storeName; public final Pattern pattern; /** 创建默认匹配,匹配所有attribute names。 */ public static AttributePattern getDefaultPattern(String storeName) { return new AttributePattern(storeName, null, null); } /** 创建精确匹配,匹配名称完全相同的attribute names。 */ public static AttributePattern getExactPattern(String storeName, String attrName) { return new AttributePattern(storeName, attrName, null); } /** 创建正则表达式匹配。 */ public static AttributePattern getRegexPattern(String storeName, String regexName) { try { return new AttributePattern(storeName, regexName, Pattern.compile(regexName)); } catch (Exception e) { throw new IllegalArgumentException(String.format("Invalid regex pattern %s for store %s", regexName, storeName)); } } private AttributePattern(String storeName, String patternName, Pattern pattern) { this.storeName = assertNotNull(trimToNull(storeName), "storeName"); this.patternName = patternName; this.pattern = pattern; } public boolean isDefaultPattern() { return patternName == null; } public boolean isRegexPattern() { return pattern != null; } public String getPatternName() { return patternName; } public String getStoreName() { return storeName; } public Pattern getPattern() { return pattern; } @Override public String toString() { ToStringBuilder buf = new ToStringBuilder(); if (isDefaultPattern()) { buf.format("match=\"*\", store=\"%s\"", storeName); } else if (isRegexPattern()) { buf.format("match=~/%s/, store=\"%s\"", patternName, storeName); } else { buf.format("match=\"%s\", store=\"%s\"", patternName, storeName); } return buf.toString(); } } }