Java tutorial
/* * Copyright 2009 Igor Azarnyi, Denys Pavlov * * 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 org.yes.cart.web.support.util.cookie.impl; import com.sun.mail.util.BASE64DecoderStream; import com.sun.mail.util.BASE64EncoderStream; import org.apache.commons.lang.StringUtils; import org.yes.cart.util.ShopCodeContext; import org.yes.cart.web.support.util.cookie.CookieTuplizer; import org.yes.cart.web.support.util.cookie.UnableToCookielizeObjectException; import org.yes.cart.web.support.util.cookie.UnableToObjectizeCookieException; import org.yes.cart.web.support.util.cookie.annotations.PersistentCookie; import javax.crypto.*; import javax.crypto.spec.DESKeySpec; import javax.servlet.http.Cookie; import java.io.*; import java.security.InvalidKeyException; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Default implementation of cookie tuplizer. Uses annotation * {@link PersistentCookie} * in order to determine group of Cookies that are needed for assembling object. * <p/> * The cookies are crypted in base64 form. According to RFC 2109 * specification cookie storage limit at least 300 cookies at least 4096 * bytes per cookie, so it allow to store 1228800 bytes aprox 1 Mb. * Base64 representation will be splited to chunks. * <p/> * User: dogma * Date: 2011-May-17 * Time: 2:17:57 PM */ public class CookieTuplizerImpl implements CookieTuplizer { private static final long serialVersionUID = 20100116L; private static final Cookie[] EMPTY_COOKIES = new Cookie[0]; private static final Map<String, Pattern> PATTERNS_CACHE = new HashMap<String, Pattern>(); private final Cipher desCipher; private final Cipher desUnCipher; private final int chunkSize; private SecretKey secretKey; /** * Default Constructor. * * @param keyRingPassword key ring password to use. * @param chunkSize Base64 chunk size. * @param secretKeyFactoryName Secret Key Factory Name. * @param cipherName Cipher name. */ public CookieTuplizerImpl(final String keyRingPassword, final int chunkSize, final String secretKeyFactoryName, final String cipherName) { this.chunkSize = chunkSize; try { final DESKeySpec desKeySpec = new DESKeySpec(keyRingPassword.getBytes()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(secretKeyFactoryName); secretKey = keyFactory.generateSecret(desKeySpec); // Create Cipher desCipher = Cipher.getInstance(cipherName); desCipher.init(Cipher.ENCRYPT_MODE, secretKey); // create uncipher desUnCipher = Cipher.getInstance(cipherName); desUnCipher.init(Cipher.DECRYPT_MODE, secretKey); } catch (Exception ike) { ShopCodeContext.getLog(this).error(ike.getMessage(), ike); throw new RuntimeException("Unable to load Cipher for CookieTuplizer", ike); } } /** * Split string to chunks by size. * * @param stringToSplit string to split. * @return array of string chunks or null if string is blank. */ String[] split(final String stringToSplit) { if (StringUtils.isNotBlank(stringToSplit)) { int strLenght = stringToSplit.length(); int splitNum = strLenght / chunkSize; if (strLenght % chunkSize > 0) { splitNum += 1; } String[] result = new String[splitNum]; for (int i = 0; i < splitNum; i++) { int startPos = i * chunkSize; // the substring starts here int endPos = startPos + chunkSize; // the substring ends here if (endPos > strLenght) { // make sure we don't cause an IndexOutOfBoundsException endPos = strLenght; } result[i] = stringToSplit.substring(startPos, endPos); } return result; } return null; } /** * Creates Cookie objects for given keys. * * @param oldCookies cookies that have come from request * @param values cookie value to be written * @param object object for which cookies are written * @return cookies that represent requested object * @throws UnableToCookielizeObjectException * thrown when annotation are not found on the object. */ Cookie[] assembleCookiesForObject(final Cookie[] oldCookies, final String[] values, final Object object) throws UnableToCookielizeObjectException { final TuplizerSetting meta = extractFromObject(object); if (meta.key == null || meta.expiry == null) { final String errMsg = "object of type: " + meta.checkedClasses.toString() + "must be annotated with PersistentCookie"; ShopCodeContext.getLog(this).error(errMsg); throw new UnableToCookielizeObjectException(errMsg); } if (values == null || values.length == 0) { return EMPTY_COOKIES; } final int previousCookieCount = countOldCookies(oldCookies, meta.key); final int currentCookieCount = values.length + 1; final Cookie[] cookies = new Cookie[Math.max(previousCookieCount, currentCookieCount)]; for (int index = 0; index < currentCookieCount - 1; index++) { cookies[index] = createNewCookie(meta.key + index, values[index], meta.expiry, meta.path); } final int terminatorIndex = currentCookieCount - 1; cookies[terminatorIndex] = createNewCookie(meta.key + (terminatorIndex), String.valueOf(meta.key.hashCode()), meta.expiry, meta.path); if (currentCookieCount < previousCookieCount) { for (int index = currentCookieCount; index < previousCookieCount; index++) { cookies[index] = createNewCookie(meta.key + index, "", 0, meta.path); // overwrite surplus cookies. } } return cookies; } Pattern getOrCreatePattern(final String key) { if (PATTERNS_CACHE.containsKey(key)) { return PATTERNS_CACHE.get(key); } final Pattern pattern = Pattern.compile(key + "(\\d+)"); PATTERNS_CACHE.put(key, pattern); return pattern; } int countOldCookies(final Cookie[] oldCookies, final String objectKey) { if (oldCookies == null || oldCookies.length == 0) { return 0; } final Pattern pattern = getOrCreatePattern(objectKey); int maxIndex = -1; for (Cookie cookie : oldCookies) { if (cookie.getName().startsWith(objectKey)) { final Matcher matcher = pattern.matcher(cookie.getName()); if (matcher.matches()) { final Integer digits = Integer.valueOf(matcher.group(1)); maxIndex = Math.max(digits, maxIndex); } } } return maxIndex + 1; } private Cookie createNewCookie(final String name, final String value, final int maxAgeInSeconds, final String path) { final Cookie cookie = new Cookie(name, value); cookie.setMaxAge(maxAgeInSeconds); cookie.setPath("/"); cookie.setVersion(1); // allow to have base64 encoded value in cookie return cookie; } /** * {@inheritDoc} */ public Cookie[] toCookies(final Cookie[] oldCookies, final Serializable serializable) throws UnableToCookielizeObjectException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); synchronized (desCipher) { BASE64EncoderStream base64EncoderStream = new BASE64EncoderStream(byteArrayOutputStream, Integer.MAX_VALUE); //will be splited manually CipherOutputStream cipherOutputStream = new CipherOutputStream(base64EncoderStream, desCipher); ObjectOutputStream objectOutputStream = null; try { objectOutputStream = new ObjectOutputStream(cipherOutputStream); objectOutputStream.writeObject(serializable); objectOutputStream.flush(); objectOutputStream.close(); } catch (Throwable ioe) { ShopCodeContext.getLog(this) .error(MessageFormat.format("Unable to serialize object {0}", serializable), ioe); throw new UnableToCookielizeObjectException(ioe); } finally { try { if (objectOutputStream != null) { objectOutputStream.close(); } cipherOutputStream.close(); base64EncoderStream.close(); byteArrayOutputStream.close(); } catch (IOException e) { ShopCodeContext.getLog(this).error("Can not close stream", e); } } } return assembleCookiesForObject(oldCookies, split(byteArrayOutputStream.toString()), serializable); } /** * Assemble cookies array to string. * * @param cookies cookies array to assemble the string * @param object the object whose representation we need to extract from cookies * @return assembled string * @throws UnableToObjectizeCookieException * thrown when annotation {@link PersistentCookie} is not found on * the object */ String assembleStringRespresentationOfObjectFromCookies(final Cookie[] cookies, final Serializable object) throws UnableToObjectizeCookieException { final TuplizerSetting meta = extractFromObject(object); if (meta.key == null) { final String errMsg = "current object of types: " + meta.checkedClasses.toString() + " must be annotated with PersistentCookie"; ShopCodeContext.getLog(this).error(errMsg); throw new UnableToObjectizeCookieException(errMsg); } final StringBuilder stringBuilder = new StringBuilder(); int cookieIndex = 0; boolean continueSearch = true; if (cookies != null) { final String terminator = String.valueOf(meta.key.hashCode()); final String objectKey = meta.key; while (continueSearch) { final String seekKey = objectKey + cookieIndex; boolean keyFound = false; for (Cookie cookie : cookies) { if (cookie.getName().equals(seekKey)) { keyFound = true; continueSearch = !terminator.equals(cookie.getValue()); if (continueSearch) { stringBuilder.append(cookie.getValue()); cookieIndex++; } break; } } if (!keyFound) { continueSearch = false; } } } return stringBuilder.toString(); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public <T extends Serializable> T toObject(Cookie[] cookies, final T object) throws UnableToObjectizeCookieException { final String input = assembleStringRespresentationOfObjectFromCookies(cookies, object); if (input.length() == 0) { return object; } final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(input.getBytes()); final BASE64DecoderStream base64DecoderStream = new BASE64DecoderStream(byteArrayInputStream); final CipherInputStream cipherInputStream = new CipherInputStream(base64DecoderStream, desUnCipher); ObjectInputStream objectInputStream = null; try { objectInputStream = new ObjectInputStream(cipherInputStream); return (T) objectInputStream.readObject(); } catch (Exception exception) { try { desUnCipher.init(Cipher.DECRYPT_MODE, secretKey); //reinit } catch (InvalidKeyException e) { ShopCodeContext.getLog(this).error("Cant reinit desUnCipher", exception); } final String errMsg = "Unable to convert bytes assembled from cookies into object"; ShopCodeContext.getLog(this).error(errMsg, exception); throw new UnableToObjectizeCookieException(errMsg, exception); } finally { try { if (objectInputStream != null) { objectInputStream.close(); } cipherInputStream.close(); base64DecoderStream.close(); byteArrayInputStream.close(); } catch (IOException ioe) { // leave this one silent as we have the object on hands. ShopCodeContext.getLog(this).error("Unable to close object stream", ioe); } } } /** * Extract meta data from object. * * @param object the object * @return meta data */ TuplizerSetting extractFromObject(final Object object) { final TuplizerSetting meta = new TuplizerSetting(); PersistentCookie cookieMeta = object.getClass().getAnnotation(PersistentCookie.class); if (cookieMeta == null) { meta.checkedClasses.append(object.getClass().getCanonicalName()).append(','); // try look on interfaces. final Class<?>[] ifaces = object.getClass().getInterfaces(); if (ifaces != null && ifaces.length > 0) { for (Class<?> iface : ifaces) { cookieMeta = iface.getAnnotation(PersistentCookie.class); if (cookieMeta == null) { meta.checkedClasses.append(iface.getCanonicalName()).append(','); } else { meta.set(cookieMeta); return meta; } } } } else { meta.set(cookieMeta); } return meta; } /** * Convenience class for meta data of object. */ static class TuplizerSetting { private String key; private Integer expiry; private String path; private StringBuilder checkedClasses = new StringBuilder(); void set(final PersistentCookie cookieMeta) { this.key = cookieMeta.value(); this.expiry = cookieMeta.expirySeconds(); this.path = cookieMeta.path(); } } }