/* * Copyright (C) NetStruxr, Inc. All rights reserved. * * This software is published under the terms of the NetStruxr * Public Software License version 0.5, a copy of which has been * included with this distribution in the LICENSE.NPL file. */ package er.extensions.appserver; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLDecoder; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.log4j.Appender; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Logger; import com.webobjects.appserver.WOAction; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOMessage; import com.webobjects.appserver.WORedirect; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WORequestHandler; import com.webobjects.appserver.WOResourceManager; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.appserver.WOTimer; import com.webobjects.appserver._private.WOComponentDefinition; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOTemporaryGlobalID; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSLog; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSProperties; import com.webobjects.foundation.NSPropertyListSerialization; import com.webobjects.foundation.NSRange; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestamp; import er.extensions.ERXExtensions; import er.extensions.ERXFrameworkPrincipal; import er.extensions.appserver.ajax.ERXAjaxApplication; import er.extensions.components.ERXAnyField; import er.extensions.components.ERXGracefulShutdown; import er.extensions.components._private.ERXActiveImage; import er.extensions.components._private.ERXWOForm; import er.extensions.components._private.ERXWORepetition; import er.extensions.components._private.ERXWOString; import er.extensions.components._private.ERXWOText; import er.extensions.components._private.ERXWOTextField; import er.extensions.eof.ERXConstant; import er.extensions.eof.ERXDatabaseContextDelegate; import er.extensions.eof.ERXEC; import er.extensions.formatters.ERXFormatterFactory; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXCompressionUtilities; import er.extensions.foundation.ERXConfigurationManager; import er.extensions.foundation.ERXExceptionUtilities; import er.extensions.foundation.ERXPatcher; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXRuntimeUtilities; import er.extensions.foundation.ERXThreadStorage; import er.extensions.foundation.ERXTimestampUtility; import er.extensions.localization.ERXLocalizer; import er.extensions.migration.ERXMigrator; import er.extensions.statistics.ERXStats; /** * ERXApplication is the abstract superclass of WebObjects applications built * with the ER frameworks.<br/> <br/> Useful enhancements include the ability * to change the deployed name of the application, support for automatic * application restarting at given intervals and more context information when * handling exceptions. */ public abstract class ERXApplication extends ERXAjaxApplication implements ERXGracefulShutdown.GracefulApplication { private static Boolean isWO54 = null; /** logging support */ public static final Logger log = Logger.getLogger(ERXApplication.class); /** request logging support */ public static final Logger requestHandlingLog = Logger.getLogger("er.extensions.ERXApplication.RequestHandling"); /** statistic logging support */ public static final Logger statsLog = Logger.getLogger("er.extensions.ERXApplication.Statistics"); private static boolean wasERXApplicationMainInvoked = false; /** * Notification to get posted when we can an OutOfMemoryError. You should * register your caching classes for this notification so you can release * memory. Registration should happen at launch time. */ public static final String LowMemoryNotification = "LowMemoryNotification"; /** * Buffer we reserve lowMemBufSize KB to release when we get an * OutOfMemoryError, so we can post our notification and do other stuff */ private static byte lowMemBuffer[]; /** * Size of the memory in KB to reserve for low-mem situations, pulled form * the system property * <code>er.extensions.ERXApplication.lowMemBufferSize</code>. Default is * 0, indicating no reserve. */ private static int lowMemBufferSize = 0; /** * Notification to post when all bundles were loaded but before their * principal was called */ public static final String AllBundlesLoadedNotification = "NSBundleAllBundlesLoaded"; /** * Notification to post when all bundles were loaded but before their * principal was called */ public static final String ApplicationDidCreateNotification = "NSApplicationDidCreateNotification"; /** * ThreadLocal that designates that the given thread is currently * dispatching a request. This is not stored in ERXThreadStorage, because it * defaults to an inheritable thread local, which would defeat the purpose * of this check. */ private static ThreadLocal<Boolean> isInRequest = new ThreadLocal<Boolean>(); private static NSDictionary propertiesFromArgv; /** * Time that garbage collection was last called when checking memory. */ private long _lastGC = 0; /** * Holds the value of the property * er.extensions.ERXApplication.memoryThreshold */ protected BigDecimal memoryThreshold; /** * The path rewriting pattern to match (@see _rewriteURL) */ protected String replaceApplicationPathPattern; /** * The path rewriting replacement to apply to the matched pattern (@see _rewriteURL) */ protected String replaceApplicationPathReplace; /** * Copies the props from the command line to the static dict * propertiesFromArgv. * */ private static void insertCommandLineArguments() { NSArray keys = propertiesFromArgv.allKeys(); int count = keys.count(); for (int i = 0; i < count; i++) { Object key = keys.objectAtIndex(i); Object value = propertiesFromArgv.objectForKey(key); NSProperties._setProperty((String) key, (String) value); } } static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader() { String classPath = System.getProperty("java.class.path"); if (System.getProperty("com.webobjects.classpath") != null) { classPath += File.pathSeparator + System.getProperty("com.webobjects.classpath"); } String files[] = classPath.split(File.pathSeparator); URL urls[] = new URL[files.length]; for (int i = 0; i < files.length; i++) { String string = files[i]; try { urls[i] = new File(string).toURL(); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return new AppClassLoader(urls, Thread.currentThread().getContextClassLoader()); } @Override public synchronized Class<?> loadClass(String s, boolean flag) throws ClassNotFoundException { SecurityManager securitymanager = System.getSecurityManager(); if (securitymanager != null) { String s1 = s.replace('/', '.'); if (s1.startsWith("[")) { int i = s1.lastIndexOf('[') + 2; if (i > 1 && i < s1.length()) { s1 = s1.substring(i); } } int j = s1.lastIndexOf('.'); if (j != -1) { securitymanager.checkPackageAccess(s1.substring(0, j)); } } return super.loadClass(s, flag); } AppClassLoader(URL aurl[], ClassLoader classloader) { super(aurl, classloader); } } private static Loader _loader; /** * Responsible for classpath munging. * * * @author ak */ public static class Loader { private JarChecker _checker; /** Holds the framework names during startup */ private Set<String> allFrameworks; private Properties allBundleProps; private static Properties readProperties(File file) { if (!file.exists()) { return null; } URL url = null; try { url = file.toURL(); } catch (MalformedURLException e) { e.printStackTrace(); } return readProperties(url); } private static Properties readProperties(URL url) { if (url == null) { return null; } Properties result = new Properties(); try { result.load(url.openStream()); } catch (MalformedURLException exception) { exception.printStackTrace(); return null; } catch (IOException exception) { exception.printStackTrace(); return null; } return result; } /** * Called prior to actually initializing the app. Defines framework load * order, class path order, checks patches etc. */ public Loader(String[] argv) { wasERXApplicationMainInvoked = true; String cps[] = new String[] { "java.class.path", "com.webobjects.classpath" }; propertiesFromArgv = NSProperties.valuesFromArgv(argv); allFrameworks = new HashSet<String>(); _checker = new JarChecker(); for (int var = 0; var < cps.length; var++) { String cpName = cps[var]; String cp = System.getProperty(cpName); if (cp != null) { String parts[] = cp.split(File.pathSeparator); String normalLibs = ""; String systemLibs = ""; String jarLibs = ""; String frameworkPattern = ".*?/(\\w+)\\.framework/Resources/Java/\\1.jar".toLowerCase(); String appPattern = ".*?/(\\w+)\\.woa/Contents/Resources/Java/\\1.jar".toLowerCase(); String folderPattern = ".*?/Resources/Java/?$".toLowerCase(); for (int i = 0; i < parts.length; i++) { String jar = parts[i]; // Windows has \, we need to normalize String fixedJar = jar.replace(File.separatorChar, '/').toLowerCase(); // System.out.println("Checking: " + jar); // all patched frameworks here if (isSystemJar(jar)) { systemLibs += jar + File.pathSeparator; } else if (fixedJar.matches(frameworkPattern) || fixedJar.matches(appPattern) || fixedJar.matches(folderPattern)) { normalLibs += jar + File.pathSeparator; } else { jarLibs += jar + File.pathSeparator; } String bundle = jar.replaceAll(".*?[/\\\\](\\w+)\\.framework.*", "$1"); String excludes = "(JavaVM)"; if (isWO54()) { excludes = "(JavaVM|JavaWebServicesSupport|JavaEODistribution|JavaWebServicesGeneration|JavaWebServicesClient)"; } if (bundle.matches("^\\w+$") && !bundle.matches(excludes)) { String info = jar.replaceAll("(.*?[/\\\\]\\w+\\.framework/Resources/).*", "$1Info.plist"); if (new File(info).exists()) { allFrameworks.add(bundle); } else { // System.out.println("Omitted: " + info); } } else if (jar.endsWith(".jar")) { String info = stringFromJar(jar, "Resources/Info.plist"); if (info != null) { NSDictionary dict = (NSDictionary) NSPropertyListSerialization.propertyListFromString(info); bundle = (String) dict.objectForKey("CFBundleExecutable"); allFrameworks.add(bundle); // System.out.println("Jar bundle: " + bundle); } } } String newCP = ""; if (normalLibs.length() > 1) { normalLibs = normalLibs.substring(0, normalLibs.length() - 1); newCP += normalLibs; } if (systemLibs.length() > 1) { systemLibs = systemLibs.substring(0, systemLibs.length() - 1); newCP += (newCP.length() > 0 ? File.pathSeparator : "") + systemLibs; } if (jarLibs.length() > 1) { jarLibs = jarLibs.substring(0, jarLibs.length() - 1); newCP += (newCP.length() > 0 ? File.pathSeparator : "") + jarLibs; } String jars[] = newCP.split(File.pathSeparator); for (int i = 0; i < jars.length; i++) { String jar = jars[i]; _checker.processJar(jar); } // AK: this is pretty experimental for now. The classpath // reordering // should actually be done in a WOLips bootstrap because as this // time all // the static inits of WO app have already happened (which // include NSMutableArray and _NSThreadSaveSet) if (System.getProperty("_DisableClasspathReorder") == null) { System.setProperty(cpName, newCP); } } } NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("bundleDidLoad", new Class[] { NSNotification.class }), "NSBundleDidLoadNotification", null); } public boolean didLoad() { return (allFrameworks != null && allFrameworks.size() == 0); } /** * Will be called after each bundle load. We use it to know when the last * bundle loaded so we can post a notification for it. Note that the bundles * will get loaded in the order of the classpath but the main bundle will * get loaded last. So in order to set the properties correctly, we first * add all the props that are not already set, then we add the main bundle * and the WebObjects.properties and finally the command line props. * * @param n */ public void bundleDidLoad(NSNotification n) { NSBundle bundle = (NSBundle) n.object(); // System.out.println(bundle.name() + ": " + allFrameworks); allFrameworks.remove(bundle.name()); if (allBundleProps == null) { allBundleProps = new Properties(); } Properties bundleProps = bundle.properties(); if (bundleProps != null) { for (Iterator iter = bundleProps.entrySet().iterator(); iter.hasNext();) { Map.Entry entry = (Map.Entry) iter.next(); if (!allBundleProps.containsKey(entry.getKey())) { allBundleProps.setProperty((String) entry.getKey(), (String) entry.getValue()); } } } if (allFrameworks.size() == 0) { Properties mainProps = null; NSBundle mainBundle = null; String mainBundleName = NSProperties._mainBundleName(); if (mainBundleName != null) { mainBundle = NSBundle.bundleForName(mainBundleName); } if (mainBundle == null) { mainBundle = NSBundle.mainBundle(); } if (mainBundle == null) { // AK: when we get here, the main bundle wasn't inited yet // so we do it ourself... try { Field ClassPath = NSBundle.class.getDeclaredField("ClassPath"); ClassPath.setAccessible(true); if (ClassPath.get(NSBundle.class) != null) { Method init = NSBundle.class.getDeclaredMethod("InitMainBundle"); init.setAccessible(true); init.invoke(NSBundle.class); } } catch (Exception e) { System.err.println(e); e.printStackTrace(); System.exit(1); } mainBundle = NSBundle.mainBundle(); } if (mainBundle != null) { mainProps = mainBundle.properties(); } if (mainProps == null) { String woUserDir = NSProperties.getProperty("webobjects.user.dir"); if (woUserDir == null) { woUserDir = System.getProperty("user.dir"); } mainProps = readProperties(new File(woUserDir, "Contents" + File.separator + "Resources" + File.separator + "Properties")); } if (mainProps == null) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Enumeration jarBundles = null; try { jarBundles = classLoader.getResources("Resources/Properties"); } catch (IOException exception) { exception.printStackTrace(); } URL propertiesPath = null; if (jarBundles != null) { while (jarBundles.hasMoreElements()) { URL url = (URL) jarBundles.nextElement(); String urlAsString = url.toString(); if (urlAsString.contains(mainBundleName + ".jar")) { try { propertiesPath = new URL(URLDecoder.decode(urlAsString, "UTF-8")); } catch (MalformedURLException exception) { exception.printStackTrace(); } catch (UnsupportedEncodingException exception) { exception.printStackTrace(); } break; } } } mainProps = readProperties(propertiesPath); } if (mainProps == null) { throw new IllegalStateException("Main bundle 'Properties' file can't be read. Did you run as a Java Application instead of a WOApplication in WOLips?\nPlease post your deployment configuration in the Wonder mailing list."); } allBundleProps.putAll(mainProps); String userhome = System.getProperty("user.home"); if (userhome != null && userhome.length() > 0) { Properties userProps = readProperties(new File(userhome, "WebObjects.properties")); if (userProps != null) { allBundleProps.putAll(userProps); } } Properties props = NSProperties._getProperties(); props.putAll(allBundleProps); NSProperties._setProperties(props); insertCommandLineArguments(); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(AllBundlesLoadedNotification, NSKeyValueCoding.NullValue)); } } private boolean isSystemJar(String jar) { // check system path String systemRoot = System.getProperty("WORootDirectory"); if (systemRoot != null) { if (jar.startsWith(systemRoot)) { return true; } } // check maven path if (jar.indexOf("webobjects" + File.separator + "apple") > 0) { return true; } // check mac path if (jar.indexOf("System" + File.separator + "Library") > 0) { return true; } // check win path if (jar.indexOf("Apple" + File.separator + "Library") > 0) { return true; } // if embedded, check explicit names if (jar.matches("Frameworks[/\\\\]Java(Foundation|EOControl|EOAccess|WebObjects).*")) { return true; } return false; } private String stringFromJar(String jar, String path) { JarFile f; try { if (!new File(jar).exists()) { ERXApplication.log.warn("Will not process jar '" + jar + "' because it cannot be found ..."); return null; } f = new JarFile(jar); JarEntry e = (JarEntry) f.getEntry(path); if (e != null) { InputStream is = f.getInputStream(e); ByteArrayOutputStream bout = new ByteArrayOutputStream(); int read = -1; byte[] buf = new byte[1024 * 50]; while ((read = is.read(buf)) != -1) { bout.write(buf, 0, read); } String content = new String(bout.toByteArray(), "UTF-8"); return content; } return null; } catch (FileNotFoundException e1) { return null; } catch (IOException e1) { throw NSForwardException._runtimeExceptionForThrowable(e1); } } } /** * Called when the application starts up and saves the command line * arguments for {@link ERXConfigurationManager}. * * @see WOApplication#main(String[], Class) */ public static void main(String argv[], Class applicationClass) { setup(argv); WOApplication.main(argv, applicationClass); } /** * Utility class to track down duplicate items in the class path. Reports * duplicate packages and packages that are present in different versions. * * @author ak */ public static class JarChecker { private static final Logger log = Logger.getLogger(JarChecker.class); private static class Entry { long _size; String _jar; public Entry(long aL, String jar) { _size = aL; _jar = jar; } public long size() { return _size; } public String jar() { return _jar; } @Override public boolean equals(Object other) { return ((Entry) other).size() == size(); } @Override public int hashCode() { return (int) _size; } @Override public String toString() { return size() + "->" + jar(); } } private static NSMutableDictionary<String, NSMutableArray<String>> packages = new NSMutableDictionary<String, NSMutableArray<String>>(); private static NSMutableDictionary<String, NSMutableSet<Entry>> classes = new NSMutableDictionary<String, NSMutableSet<Entry>>(); private void processJar(String jar) { try { File jarFile = new File(jar); if (!jarFile.exists() || jarFile.isDirectory()) { return; } JarFile f = new JarFile(jar); for (Enumeration enumerator = f.entries(); enumerator.hasMoreElements();) { JarEntry entry = (JarEntry) enumerator.nextElement(); String name = entry.getName(); if (entry.getName().endsWith("/") && !(name.matches("^\\w+/$") || name.startsWith("META-INF"))) { NSMutableArray<String> bundles = packages.objectForKey(name); if (bundles == null) { bundles = new NSMutableArray<String>(); packages.setObjectForKey(bundles, name); } bundles.addObject(jar); } else if (!(name.startsWith("src") || name.startsWith("META-INF"))) { Entry e = new Entry(entry.getSize(), jar); NSMutableSet<Entry> set = classes.objectForKey(name); if (set == null) { set = new NSMutableSet<Entry>(); classes.setObjectForKey(set, name); } set.addObject(e); } } } catch (IOException e) { // AK AK TODO: Auto-generated catch block log.error(e, e); } } private void reportErrors() { StringBuffer sb = new StringBuffer(); String message = null; NSArray<String> keys = ERXArrayUtilities.sortedArraySortedWithKey(packages.allKeys(), "toString"); for (Enumeration<String> enumerator = keys.objectEnumerator(); enumerator.hasMoreElements();) { String packageName = enumerator.nextElement(); NSMutableArray<String> bundles = packages.objectForKey(packageName); if (bundles.count() > 1) { sb.append("\t").append(packageName).append("->").append(bundles).append("\n"); } } message = sb.toString(); if (message.length() > 0) { log.info("The following packages appear multiple times:\n" + message); } sb = new StringBuffer(); NSMutableSet<String> classPackages = new NSMutableSet<String>(); keys = ERXArrayUtilities.sortedArraySortedWithKey(classes.allKeys(), "toString"); for (Enumeration<String> enumerator = keys.objectEnumerator(); enumerator.hasMoreElements();) { String className = enumerator.nextElement(); String packageName = className.replaceAll("/[^/]+?$", ""); NSMutableSet<Entry> bundles = classes.objectForKey(className); if (bundles.count() > 1 && !classPackages.containsObject(packageName)) { sb.append("\t").append(packageName).append("->").append(bundles).append("\n"); classPackages.addObject(packageName); } } message = sb.toString(); if (message.length() > 0) { log.warn("The following packages have different versions, you should remove the version you don't want:\n" + message); } } } /** * Called prior to actually initializing the app. Defines framework load * order, class path order, checks patches etc. */ public static void setup(String[] argv) { if (_loader == null) { // System.out.println("setup(" + Arrays.toString(argv) + ")"); // System.out.println(" _DisableClasspathReorder = " // + System.getProperty("_DisableClasspathReorder")); _loader = new Loader(argv); if (System.getProperty("_DisableClasspathReorder") == null) { ClassLoader loader = AppClassLoader.getAppClassLoader(); Thread.currentThread().setContextClassLoader(loader); } if (!ERXApplication.isWO54()) { Class arrayClass = NSMutableArray.class; try { @SuppressWarnings("unused") Field f = arrayClass.getField("ERX_MARKER"); } catch (NoSuchFieldException e) { System.err.println("No ERX_MARKER field in NSMutableArray found. \nThis means your class path is incorrect. Adjust it so that ERExtensions come before JavaFoundation."); System.exit(1); } catch (Exception e) { e.printStackTrace(); } } ERXConfigurationManager.defaultManager().setCommandLineArguments(argv); ERXFrameworkPrincipal.setUpFrameworkPrincipalClass(ERXExtensions.class); ERXStats.initStatisticsIfNecessary(); } } /** * Installs several bufixes and enhancements to WODynamicElements. Sets the * Context class name to "er.extensions.ERXWOContext" if it is "WOContext". * Patches ERXWOForm, ERXWOFileUpload, ERXWOText to be used instead of * WOForm, WOFileUpload, WOText. */ public void installPatches() { ERXPatcher.installPatches(); if (contextClassName().equals("WOContext")) { if (ERXApplication.isWO54()) { setContextClassName("ERXWOContext54"); } else { setContextClassName(ERXWOContext.class.getName()); } } if (contextClassName().equals("WOServletContext") || contextClassName().equals("com.webobjects.appserver.jspservlet.WOServletContext")) setContextClassName(ERXWOServletContext.class.getName()); ERXPatcher.setClassForName(ERXWOForm.class, "WOForm"); try { ERXPatcher.setClassForName(ERXAnyField.class, "WOAnyField"); } catch (NoClassDefFoundError e) { ERXApplication.log.info("JavaWOExtensions is not loaded, so WOAnyField will not be patched."); } ERXPatcher.setClassForName(ERXWORepetition.class, "WORepetition"); ERXPatcher.setClassForName(ERXActiveImage.class, "WOActiveImage"); // use our localizing string class // works around #3574558 if (ERXLocalizer.isLocalizationEnabled()) { ERXPatcher.setClassForName(ERXWOString.class, "WOString"); ERXPatcher.setClassForName(ERXWOTextField.class, "WOTextField"); } // ERXPatcher.setClassForName(ERXSubmitButton.class, "WOSubmitButton"); // Fix for 3190479 URI encoding should always be UTF8 // See http://www.w3.org/International/O-URL-code.html // For WO 5.1.x users, please comment this statement to compile. com.webobjects.appserver._private.WOURLEncoder.WO_URL_ENCODING = "UTF8"; // WO 5.1 specific patches if (ERXProperties.webObjectsVersionAsDouble() < 5.2d) { // ERXWOText contains a patch for WOText to not include the value // attribute (#2948062). Fixed in WO 5.2 ERXPatcher.setClassForName(ERXWOText.class, "WOText"); } // ERXWOFileUpload returns a better warning than throwing a // ClassCastException. // Fixed in WO 5.2 // ERXPatcher.setClassForName(ERXWOFileUpload.class, "WOFileUpload"); } @Override public WOResourceManager createResourceManager() { return new ERXResourceManager(); } /** * The ERXApplication contructor. */ public ERXApplication() { super(); if (!ERXConfigurationManager.defaultManager().isDeployedAsServlet() && (!wasERXApplicationMainInvoked || _loader == null)) { _displayMainMethodWarning(); } if (_loader == null || !_loader.didLoad()) { throw new RuntimeException("ERXExtensions have not been initialized. Please report the classpath and the rest of the bundles to the Wonder mailing list: " + "\nRemaining frameworks: " + _loader.allFrameworks + "\nClasspath: " + System.getProperty("java.class.path")); } if ("JavaFoundation".equals(NSBundle.mainBundle().name())) { throw new RuntimeException("Your main bundle is \"JavaFoundation\". You are not launching this WO application properly. If you are using Eclipse, most likely you launched your WOA as a \"Java Application\" instead of a \"WO Application\"."); } // ak: telling Log4J to re-init the Console appenders so we get logging // into WOOutputPath again for (Enumeration e = Logger.getRootLogger().getAllAppenders(); e.hasMoreElements();) { Appender appender = (Appender) e.nextElement(); if (appender instanceof ConsoleAppender) { ConsoleAppender app = (ConsoleAppender) appender; app.activateOptions(); } } _loader._checker.reportErrors(); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(ApplicationDidCreateNotification, this)); installPatches(); lowMemBufferSize = ERXProperties.intForKeyWithDefault("er.extensions.ERXApplication.lowMemBufferSize", 0); if (lowMemBufferSize > 0) { lowMemBuffer = new byte[lowMemBufferSize]; } registerRequestHandler(new ERXDirectActionRequestHandler(), directActionRequestHandlerKey()); if (isDirectConnectEnabled()) { registerRequestHandler(new ERXStaticResourceRequestHandler(), "_wr_"); } registerRequestHandler(new ERXDirectActionRequestHandler(ERXDirectAction.class.getName(), "stats", false), "erxadm"); // AK: remove comment to get delayed request handling // registerRequestHandler(new DelayedRequestHandler(), DelayedRequestHandler.KEY); Long timestampLag = Long.getLong("EOEditingContextDefaultFetchTimestampLag"); if (timestampLag != null) EOEditingContext.setDefaultFetchTimestampLag(timestampLag.longValue()); String defaultEncoding = System.getProperty("er.extensions.ERXApplication.DefaultEncoding"); if (defaultEncoding != null) { log.debug("Setting default encoding to \"" + defaultEncoding + "\""); setDefaultEncoding(defaultEncoding); } String defaultMessageEncoding = System.getProperty("er.extensions.ERXApplication.DefaultMessageEncoding"); if (defaultMessageEncoding != null) { log.debug("Setting WOMessage default encoding to \"" + defaultMessageEncoding + "\""); WOMessage.setDefaultEncoding(defaultMessageEncoding); } // Configure the WOStatistics CLFF logging since it can't be controled // by a property, grrr. configureStatisticsLogging(); NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("finishInitialization", ERXConstant.NotificationClassArray), WOApplication.ApplicationWillFinishLaunchingNotification, null); NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("didFinishLaunching", ERXConstant.NotificationClassArray), WOApplication.ApplicationDidFinishLaunchingNotification, null); Boolean useUnlocker = useEditingContextUnlocker(); if (useUnlocker != null) { ERXEC.setUseUnlocker(useUnlocker); } Boolean traceOpenLocks = traceOpenEditingContextLocks(); if (traceOpenLocks != null) { ERXEC.setTraceOpenLocks(traceOpenLocks); } // Signal handling support if (ERXGracefulShutdown.isEnabled()) { ERXGracefulShutdown.installHandler(); } // AK: this makes it possible to retrieve the creating instance from an // NSData PK. // it should still be unique, as one host can only have one running // instance to a port EOTemporaryGlobalID._setProcessIdentificationBytesFromInt(port().intValue()); memoryThreshold = ERXProperties.bigDecimalForKey("er.extensions.ERXApplication.memoryThreshold"); replaceApplicationPathPattern = ERXProperties.stringForKey("er.extensions.ERXApplication.replaceApplicationPath.pattern"); if (replaceApplicationPathPattern != null && replaceApplicationPathPattern.length() == 0) { replaceApplicationPathPattern = null; } replaceApplicationPathReplace = ERXProperties.stringForKey("er.extensions.ERXApplication.replaceApplicationPath.replace"); if (replaceApplicationPathPattern == null && rewriteDirectConnectURL()) { replaceApplicationPathPattern = "/cgi-bin/WebObjects/" + name() + ".woa"; if (replaceApplicationPathReplace == null) { replaceApplicationPathReplace = ""; } } } /** * Decides whether to use editing context unlocking. * * @return true if ECs should be unlocked after each RR-loop * @deprecated use er.extensions.ERXEC.useUnlocker property instead */ public Boolean useEditingContextUnlocker() { Boolean useUnlocker = null; if (ERXProperties.stringForKey("er.extensions.ERXApplication.useEditingContextUnlocker") != null) { useUnlocker = Boolean.valueOf(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.useEditingContextUnlocker", false)); } return useUnlocker; } /** * Decides whether or not to keep track of open editing context locks. * * @return true if editing context locks should be tracked * @deprecated use er.extensions.ERXEC.traceOpenLocks property instead */ public Boolean traceOpenEditingContextLocks() { Boolean traceOpenLocks = null; if (ERXProperties.stringForKey("er.extensions.ERXApplication.traceOpenEditingContextLocks") != null) { traceOpenLocks = Boolean.valueOf(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.traceOpenEditingContextLocks", false)); } return traceOpenLocks; } /** * Configures the statistics logging for a given application. By default * will log to a file <base log directory>/<WOApp Name>-<host>-<port>.log * if the base log path is defined. The base log path is defined by the * property <code>er.extensions.ERXApplication.StatisticsBaseLogPath</code> * The default log rotation frequency is 24 hours, but can be changed by * setting in milliseconds the property * <code>er.extensions.ERXApplication.StatisticsLogRotationFrequency</code> */ public void configureStatisticsLogging() { String statisticsBasePath = System.getProperty("er.extensions.ERXApplication.StatisticsBaseLogPath"); if (statisticsBasePath != null) { // Defaults to a single day int rotationFrequency = ERXProperties.intForKeyWithDefault("er.extensions.ERXApplication.StatisticsLogRotationFrequency", 24 * 60 * 60 * 1000); String logPath = statisticsBasePath + File.separator + name() + "-" + ERXConfigurationManager.defaultManager().hostName() + "-" + port() + ".log"; if (log.isDebugEnabled()) { log.debug("Configured statistics logging to file path \"" + logPath + "\" with rotation frequency: " + rotationFrequency); } statisticsStore().setLogFile(logPath, rotationFrequency); } } /** * Notification method called when the application posts the notification * {@link WOApplication#ApplicationWillFinishLaunchingNotification}. This * method calls subclasse's {@link #finishInitialization} method. * * @param n * notification that is posted after the WOApplication has been * constructed, but before the application is ready for accepting * requests. */ public final void finishInitialization(NSNotification n) { finishInitialization(); if (ERXMigrator.shouldMigrateAtStartup()) { migrator().migrateToLatest(); } } /** * Notification method called when the application posts the notification * {@link WOApplication#ApplicationDidFinishLaunchingNotification}. This * method calls subclasse's {@link #didFinishLaunching} method. * * @param n * notification that is posted after the WOApplication has * finished launching and is ready for accepting requests. */ public final void didFinishLaunching(NSNotification n) { didFinishLaunching(); ERXStats.logStatisticsForOperation(statsLog, "sum"); // ERXStats.reset(); if (isDevelopmentMode() && !autoOpenInBrowser()) { log.warn("You are running in development mode with WOAutoOpenInBrowser = false. No browser will open and it will look like the application is hung, but it's not. There's just not a browser opening automatically."); } } /** * Called when the application posts * {@link WOApplication#ApplicationWillFinishLaunchingNotification}. * Override this to perform application initialization. (optional) */ public void finishInitialization() { // empty } /** * Called when the application posts * {@link WOApplication#ApplicationDidFinishLaunchingNotification}. * Override this to perform application specific tasks after the application * has been initialized. THis is a good spot to perform batch application * tasks. */ public void didFinishLaunching() { // empty } /** * The ERXApplication singleton. * * @return returns the <code>WOApplication.application()</code> cast as an * ERXApplication */ public static ERXApplication erxApplication() { return (ERXApplication) WOApplication.application(); } /** * Adds support for automatic application cycling. Applications can be * configured to cycle in two ways:<br/> <br/> The first way is by setting * the System property <b>ERTimeToLive</b> to the number of seconds (+ a * random interval of 10 minutes) that the application should be up before * terminating. Note that when the application's time to live is up it will * quit calling the method <code>killInstance</code>.<br/> <br/> The * second way is by setting the System property <b>ERTimeToDie</b> to the * time in seconds after midnight when the app should be starting to refuse * new sessions. In this case when the application starts to refuse new * sessions it will also register a kill timer that will terminate the * application between 0 minutes and 1:00 minutes.<br/> */ @Override public void run() { try { int timeToLive = ERXProperties.intForKey("ERTimeToLive"); if (timeToLive > 0) { log.info("Instance will live " + timeToLive + " seconds."); NSLog.out.appendln("Instance will live " + timeToLive + " seconds."); // add a fudge factor of around 10 minutes timeToLive += (new Random()).nextFloat() * 600; NSTimestamp exitDate = (new NSTimestamp()).timestampByAddingGregorianUnits(0, 0, 0, 0, 0, timeToLive); WOTimer t = new WOTimer(exitDate, 0, this, "killInstance", null, null, false); t.schedule(); } int timeToDie = ERXProperties.intForKey("ERTimeToDie"); if (timeToDie > 0) { log.info("Instance will not live past " + timeToDie + ":00."); NSLog.out.appendln("Instance will not live past " + timeToDie + ":00."); NSTimestamp now = new NSTimestamp(); int s = (timeToDie - ERXTimestampUtility.hourOfDay(now)) * 3600 - ERXTimestampUtility.minuteOfHour(now) * 60; if (s < 0) s += 24 * 3600; // how many seconds to the deadline // deliberately randomize this so that not all instances restart at // the same time // adding up to 1 hour s += (new Random()).nextFloat() * 3600; NSTimestamp stopDate = now.timestampByAddingGregorianUnits(0, 0, 0, 0, 0, s); WOTimer t = new WOTimer(stopDate, 0, this, "startRefusingSessions", null, null, false); t.schedule(); } super.run(); } catch (RuntimeException t) { if (ERXApplication._wasMainInvoked) { ERXApplication.log.error(name() + " failed to start.", t); //throw new ERXExceptionUtilities.HideStackTraceException(t); } throw t; } } /** * Creates the request object for this loop. Calls _createRequest(). For WO * 5.3. */ @SuppressWarnings("all") // Suppress @Override warning for 5.4 public WORequest createRequest(String aMethod, String aURL, String anHTTPVersion, NSDictionary someHeaders, NSData aContent, NSDictionary someInfo) { return _createRequest(aMethod, aURL, anHTTPVersion, someHeaders, aContent, someInfo); } /** * Creates the request object for this loop. Calls _createRequest(). For WO * 5.4. */ @SuppressWarnings("all") // Suppress @Override warning for 5.3 public WORequest createRequest(String method, String aurl, String anHTTPVersion, Map<String, ? extends List<String>> someHeaders, NSData content, Map<String, Object> someInfo) { return _createRequest(method, aurl, anHTTPVersion, (someHeaders != null ? new NSDictionary<String, Object>(someHeaders, true) : null), content, (someInfo != null ? new NSDictionary<String, Object>(someInfo, true) : null)); } /** * Bottleneck for WORequest creation in WO 5.3 and 5.4 to use an * {@link ERXRequest} object that fixes a bug with localization. */ protected WORequest _createRequest(String aMethod, String aURL, String anHTTPVersion, NSDictionary someHeaders, NSData aContent, NSDictionary someInfo) { // Workaround for #3428067 (Apache Server Side Include module will feed // "INCLUDED" as the HTTP version, which causes a request object not to // be // created by an exception. if (anHTTPVersion == null || anHTTPVersion.startsWith("INCLUDED")) { anHTTPVersion = "HTTP/1.0"; } // Workaround for Safari on Leopard bug (post followed by redirect to GET incorrectly has content-type header). // The content-type header makes the WO parser only look at the content. Which is empty. // http://lists.macosforge.org/pipermail/webkit-unassigned/2007-November/053847.html // http://jira.atlassian.com/browse/JRA-13791 if ("GET".equalsIgnoreCase(aMethod) && someHeaders != null && someHeaders.objectForKey("content-type") != null) { someHeaders = someHeaders.mutableClone(); ((NSMutableDictionary)someHeaders).removeObjectForKey("content-type"); } if (rewriteDirectConnectURL()) { aURL = "/cgi-bin/WebObjects/" + name() + ".woa" + aURL; } WORequest worequest = new ERXRequest(aMethod, aURL, anHTTPVersion, someHeaders, aContent, someInfo); return worequest; } /** * Used to instantiate a WOComponent when no context is available, typically * outside of a session * * @param pageName - * The name of the WOComponent that must be instantiated. * @return created WOComponent with the given name */ public static WOComponent instantiatePage(String pageName) { WOContext context = ERXWOContext.newContext(); return application().pageWithName(pageName, context); } /** * Stops the application from handling any new requests. Will still handle * requests from existing sessions. */ public void startRefusingSessions() { log.info("Refusing new sessions"); NSLog.out.appendln("Refusing new sessions"); refuseNewSessions(true); } protected WOTimer _killTimer; /** * Bugfix for WO component loading. It fixes: * <ul> * <li> when isCachingEnabled is ON, and you have a new browser language * that hasn't been seen so far, the component gets re-read from the disk, * which can wreak havoc if you overwrite your html/wod with a new version. * <li> when caching enabled is OFF, and you make a change, you only see the * change in the first browser that touches the page. You need to re-save if * you want it seen in the second one. * </ul> * You need to set * <code>er.extensions.ERXApplication.fixCachingEnabled=false</code> is * you don't want it to load. * * @author ak */ public WOComponentDefinition _componentDefinition(String s, NSArray nsarray) { if(ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.fixCachingEnabled", true)) { // _expectedLanguages already contains all the languages in all projects, so // there is no need to check for the ones that come in... return super._componentDefinition(s, _expectedLanguages()); } return super._componentDefinition(s, nsarray); } private boolean _isMemoryStarved = false; /** * Checks if the free memory is less than the threshold given in * <code>er.extensions.ERXApplication.memoryThreshold</code> (should be * set to around 0.90 meaning 90% of total memory or 100 meaning 100 MB of * minimal available memory) and if it is greater start to refuse new * sessions until more memory becomes available. This helps when the * application is becoming unresponsive because it's more busy garbage * collecting than processing requests. The default is to do nothing unless * the property is set. This method is called on each request, but garbage * collection will be done only every minute. * * @author ak */ protected void checkMemory() { if (memoryThreshold != null) { long max = Runtime.getRuntime().maxMemory(); long total = Runtime.getRuntime().totalMemory(); long free = Runtime.getRuntime().freeMemory() + (max - total); long used = max - free; long threshold = (long) (memoryThreshold.doubleValue() < 1.0 ? memoryThreshold.doubleValue() * max : (max - (memoryThreshold.doubleValue() * 1024 * 1024))); synchronized (this) { long time = System.currentTimeMillis(); if ((used > threshold) && (time > _lastGC + 60 * 1000L)) { _lastGC = time; Runtime.getRuntime().gc(); max = Runtime.getRuntime().maxMemory(); total = Runtime.getRuntime().totalMemory(); free = Runtime.getRuntime().freeMemory() + (max - total); used = max - free; } boolean isStarved = (used > threshold); if(isStarved != _isMemoryStarved) { if(!isStarved) { log.warn("App is no longer starved, handling new sessions again"); } else { log.error("App is starved, starting to refuse new sessions"); } _isMemoryStarved = isStarved; } } } } /** * Overridden to return the super value OR true if the app is memory starved. */ @Override public boolean isRefusingNewSessions() { return super.isRefusingNewSessions() || _isMemoryStarved; } /** * Overridden to install/uninstall a timer that will terminate the * application in <code>ERTimeToKill</code> seconds from the time this * method is called. The timer will get uninstalled if you allow new * sessions again during that time span. */ @Override public synchronized void refuseNewSessions(boolean value) { super.refuseNewSessions(value); resetKillTimer( isRefusingNewSessions()); } /** * Sets the kill timer. * @param install */ private void resetKillTimer(boolean install) { // we assume that we changed our mind about killing the instance. if (_killTimer != null) { _killTimer.invalidate(); _killTimer = null; } if (install) { int timeToKill = ERXProperties.intForKey("ERTimeToKill"); if (timeToKill > 0) { log.warn("Registering kill timer in " + timeToKill + "seconds"); NSTimestamp exitDate = (new NSTimestamp()).timestampByAddingGregorianUnits(0, 0, 0, 0, 0, timeToKill); _killTimer = new WOTimer(exitDate, 0, this, "killInstance", null, null, false); _killTimer.schedule(); } } } /** * Killing the instance will log a 'Forcing exit' message and then call * <code>System.exit(1)</code> */ public void killInstance() { log.info("Forcing exit"); NSLog.out.appendln("Forcing exit"); System.exit(1); } /** cached name suffix */ private String _nameSuffix; /** has the name suffix been cached? */ private boolean _nameSuffixLookedUp = false; /** * The name suffix is appended to the current name of the application. This * adds the ability to add a useful suffix to differentiate between * different sets of applications on the same machine.<br/> <br/> The name * suffix is set via the System property <b>ERApplicationNameSuffix</b>.<br/> * <br/> For example if the name of an application is Buyer and you want to * have a training instance appear with the name BuyerTraining then you * would set the ERApplicationNameSuffix to Training.<br/> <br/> * * @return the System property <b>ERApplicationNameSuffix</b> or * <code>null</code> */ public String nameSuffix() { if (!_nameSuffixLookedUp) { _nameSuffix = System.getProperty("ERApplicationNameSuffix"); _nameSuffix = _nameSuffix == null ? "" : _nameSuffix; _nameSuffixLookedUp = true; } return _nameSuffix; } /** cached computed name */ private String _userDefaultName; /** * Adds the ability to completely change the applications name by setting * the System property <b>ERApplicationName</b>. Will also append the * <code>nameSuffix</code> if one is set.<br/> <br/> * * @return the computed name of the application. */ @Override public String name() { if (_userDefaultName == null) { _userDefaultName = System.getProperty("ERApplicationName"); if (_userDefaultName == null) _userDefaultName = super.name(); if (_userDefaultName != null) { String suffix = nameSuffix(); if (suffix != null && suffix.length() > 0) _userDefaultName += suffix; } } return _userDefaultName; } /** * This method returns {@link WOApplication}'s <code>name</code> method.<br/> * * @return the name of the application executable. */ public String rawName() { return super.name(); } /** * Puts together a dictionary with a bunch of useful information relative to * the current state when the exception occurred. Potentially added * information:<br/> * <ol> * <li>the current page name</li> * <li>the current component</li> * <li>the complete hierarchy of nested components</li> * <li>the requested uri</li> * <li>the D2W page configuration</li> * <li>the previous page list (from the WOStatisticsStore)</li> * </ol> * Also, in case the top-level exception was a EOGeneralAdaptorException, * then you also get the failed ops and the sql exception. <br/> * * @return dictionary containing extra information for the current context. */ public NSMutableDictionary extraInformationForExceptionInContext(Exception e, WOContext context) { NSMutableDictionary<String, Object> extraInfo = ERXRuntimeUtilities.informationForException(e); extraInfo.addEntriesFromDictionary(ERXRuntimeUtilities.informationForContext(context)); extraInfo.addEntriesFromDictionary(ERXRuntimeUtilities.informationForBundles()); return extraInfo; } /** * Reports an exception. This method only logs the error and could be * overriden to return a valid error page. * * @param exception * to be reported * @param context * for the exception * @param extraInfo * dictionary of extra information about what was happening when * the exception was thrown. * @return a valid response to display or null. In that case the * superclasses {@link #handleException(Exception, WOContext)} is * called */ public WOResponse reportException(Throwable exception, WOContext context, NSDictionary extraInfo) { log.error("Exception caught: " + exception.getMessage() + "\nExtra info: " + NSPropertyListSerialization.stringFromPropertyList(extraInfo) + "\n", exception); return null; } /** * Workaround for WO 5.2 DirectAction lock-ups. As the super-implementation * is empty, it is fairly safe to override here to call the normal exception * handling earlier than usual. * * @see WOApplication#handleActionRequestError(WORequest, Exception, String, * WORequestHandler, String, String, Class, WOAction) */ // NOTE: if you use WO 5.1, comment out this method, otherwise it won't // compile. @Override public WOResponse handleActionRequestError(WORequest aRequest, Exception exception, String reason, WORequestHandler aHandler, String actionClassName, String actionName, Class actionClass, WOAction actionInstance) { WOContext context = actionInstance != null ? actionInstance.context() : null; if(context == null) { // AK: we provide the "handleException" with not much enough info to output a reasonable error message context = createContextForRequest(aRequest); } WOResponse response = handleException(exception, context); // AK: bugfix for #4186886 (Session store deadlock with DAs). The bug // occurs in 5.2.3, I'm not sure about other // versions. // It may create other problems, but this one is very severe to begin // with // The crux of the matter is that for certain exceptions, the DA request // handler does not check sessions back in // which leads to a deadlock in the session store when the session is // accessed again. if (context != null && context.hasSession() && ("InstantiationError".equals(reason) || "InvocationError".equals(reason))) { context._putAwakeComponentsToSleep(); saveSessionForContext(context); } return response; } /** * Logs extra information about the current state. * * @param exception * to be handled * @param context * current context * @return the WOResponse of the generated exception page. */ @Override public WOResponse handleException(Exception exception, WOContext context) { if (ERXProperties.booleanForKey("er.extensions.ERXApplication.redirectOnMissingObjects")) { // AK: the idea here is that you might have a stale object that was // deleted from the DB // while you weren't looking so the next time around your page might // get a chance earlier to // realize it isn't there anymore. Unfortunately, this doesn't work // in all scenarios. if (exception instanceof ERXDatabaseContextDelegate.ObjectNotAvailableException && context != null) { String retryKey = context.request().stringFormValueForKey("ERXRetry"); if (retryKey == null) { WORedirect page = new WORedirect(context); page.setUrl(context.request().uri() + "?ERXRetry=1"); return page.generateResponse(); } } } // We first want to test if we ran out of memory. If so we need to quit // ASAP. handlePotentiallyFatalException(exception); // Not a fatal exception, business as usual. NSDictionary extraInfo = extraInformationForExceptionInContext(exception, context); WOResponse response = reportException(exception, context, extraInfo); if (response == null) response = super.handleException(exception, context); return response; } /** * Standard exception page. Also logs error to standard out. * * @param exception * to be handled * @param context * current context * @return the WOResponse of the generic exception page. */ public WOResponse genericHandleException(Exception exception, WOContext context) { return super.handleException(exception, context); } /** * Handles the potentially fatal OutOfMemoryError by quitting the * application ASAP. Broken out into a separate method to make custom error * handling easier, ie. generating your own error pages in production, etc. * * @param exception * to check if it is a fatal exception. */ public void handlePotentiallyFatalException(Exception exception) { Throwable throwable = ERXRuntimeUtilities.originalThrowable(exception); if (throwable instanceof Error) { boolean shouldQuit = false; if (throwable instanceof OutOfMemoryError) { shouldQuit = true; // AK: I'm not sure this actually works, in particular when the // buffer is in the long-running generational mem, but it's // worth a try. // what we do is set up a last-resort buffer during startup if (lowMemBuffer != null) { Runtime.getRuntime().freeMemory(); try { lowMemBuffer = null; System.gc(); log.error("Ran out of memory, sending notification to clear caches"); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(LowMemoryNotification, this)); shouldQuit = false; // try to reclaim our twice of our buffer // if this worked maybe we can continue running lowMemBuffer = new byte[lowMemBufferSize * 2]; // shrink buffer to normal size lowMemBuffer = new byte[lowMemBufferSize]; } catch (Throwable ex) { shouldQuit = true; } } // We first log just in case the log4j call puts us in a bad // state. if (shouldQuit) { NSLog.err.appendln("Ran out of memory, killing this instance"); log.error("Ran out of memory, killing this instance"); } } else { // We log just in case the log4j call puts us in a bad // state. NSLog.err.appendln("java.lang.Error \"" + throwable.getClass().getName() + "\" occurred."); log.error("java.lang.Error \"" + throwable.getClass().getName() + "\" occurred.", throwable); } if (shouldQuit) Runtime.getRuntime().exit(1); } } /** use the redirect feature */ protected Boolean useComponentActionRedirection; /** * Set the * <code>er.extensions.ERXComponentActionRedirector.enabled=true</code> * property to actually the redirect feature. * * @return flag if to use the redirect feature */ public boolean useComponentActionRedirection() { if (useComponentActionRedirection == null) { useComponentActionRedirection = ERXProperties.booleanForKey("er.extensions.ERXComponentActionRedirector.enabled") ? Boolean.TRUE : Boolean.FALSE; } return useComponentActionRedirection.booleanValue(); } /** * Overridden to allow for redirected responses. * * @param request * object * @param context * object */ @Override public WOActionResults invokeAction(WORequest request, WOContext context) { WOActionResults results = super.invokeAction(request, context); if (useComponentActionRedirection()) { ERXComponentActionRedirector.createRedirector(results); } return results; } /** * Overridden to allow for redirected responses. * * @param response * object * @param context * object */ @Override public void appendToResponse(WOResponse response, WOContext context) { super.appendToResponse(response, context); if (useComponentActionRedirection()) { ERXComponentActionRedirector redirector = ERXComponentActionRedirector.currentRedirector(); if (redirector != null) { redirector.setOriginalResponse(response); } } } /** * Initializes the current thread for a request. */ public static void _startRequest() { ERXApplication.isInRequest.set(Boolean.TRUE); } /** * Cleans up the current thread after a request is complete. */ // CHECKME: as one can call dispatchRequest() in normal code, it may not be such a good // to clean everything... public static void _endRequest() { ERXApplication.isInRequest.remove(); // We always want to clean up the thread storage variables, so they // don't end up on // someone else's thread by accident ERXThreadStorage.reset(); // We *always* want to unlock left over ECs. ERXEC.unlockAllContextsForCurrentThread(); // we don't want this hanging around ERXRuntimeUtilities.clearThreadInterrupt(Thread.currentThread()); } /** * Returns true if the current thread is dispatching a request. * * @return true if the current thread is dispatching a request */ public static boolean isInRequest() { return ERXApplication.isInRequest.get() != null; } /** * Returns the delayedRequestHandler, if any is registered. * @return */ public ERXDelayedRequestHandler delayedRequestHandler() { return (ERXDelayedRequestHandler) requestHandlerForKey(ERXDelayedRequestHandler.KEY); } /** * Overridden to allow for redirected responses and null the thread local * storage. * * @param request * object * @return response */ @Override public WOResponse dispatchRequest(WORequest request) { WOResponse response = null; ERXDelayedRequestHandler delayedRequestHandler = delayedRequestHandler(); if(delayedRequestHandler == null) { response = dispatchRequestImmediately(request); } else { response = delayedRequestHandler.handleRequest(request); } return response; } /** * Dispatches the request without checking for the delayedRequestHandler() * @param request * @return */ public WOResponse dispatchRequestImmediately(WORequest request) { WOResponse response; if (ERXApplication.requestHandlingLog.isDebugEnabled()) { ERXApplication.requestHandlingLog.debug(request); } try { ERXApplication._startRequest(); ERXStats.initStatisticsIfNecessary(); checkMemory(); if (useComponentActionRedirection()) { ERXComponentActionRedirector redirector = ERXComponentActionRedirector.redirectorForRequest(request); if (redirector == null) { response = super.dispatchRequest(request); redirector = ERXComponentActionRedirector.currentRedirector(); if (redirector != null) { response = redirector.redirectionResponse(); } } else { response = redirector.originalResponse(); } } else { response = super.dispatchRequest(request); } } finally { ERXStats.logStatisticsForOperation(statsLog, "sum"); ERXApplication._endRequest(); } if (requestHandlingLog.isDebugEnabled()) { requestHandlingLog.debug("Returning, encoding: " + response.contentEncoding() + " response: " + response); } if (responseCompressionEnabled()) { String contentType = response.headerForKey("content-type"); if (!"gzip".equals(response.headerForKey("content-encoding")) && (contentType != null) && (contentType.startsWith("text/") || contentType.equals("application/x-javascript"))) { String acceptEncoding = request.headerForKey("accept-encoding"); if ((acceptEncoding != null) && (acceptEncoding.toLowerCase().indexOf("gzip") != -1)) { long start = System.currentTimeMillis(); long inputBytesLength; InputStream contentInputStream = response.contentInputStream(); byte[] compressedData; if (contentInputStream != null) { inputBytesLength = response.contentInputStreamLength(); NSData compressedNSData = ERXCompressionUtilities.gzipInputStreamAsNSData(contentInputStream, (int)inputBytesLength); compressedData = compressedNSData._bytesNoCopy(); response.setContentStream(null, 0, 0); } else { NSData input = response.content(); byte[] inputBytes = input._bytesNoCopy(); inputBytesLength = inputBytes.length; compressedData = ERXCompressionUtilities.gzipByteArray(inputBytes); } if (compressedData == null) { // something went wrong } else { response.setContent(new NSData(compressedData, new NSRange(0, compressedData.length), true)); response.setHeader(String.valueOf(compressedData.length), "content-length"); response.setHeader("gzip", "content-encoding"); if (log.isDebugEnabled()) { log.debug("before: " + inputBytesLength + ", after " + compressedData.length + ", time: " + (System.currentTimeMillis() - start)); } } } } } return response; } /** * When a context is created we push it into thread local storage. This * handles the case for direct actions. * * @param request * the request * @return the newly created context */ @Override public WOContext createContextForRequest(WORequest request) { WOContext context = super.createContextForRequest(request); // We only want to push in the context the first time it is // created, ie we don't want to lose the current context // when we create a context for an error page. if (ERXWOContext.currentContext() == null) { ERXWOContext.setCurrentContext(context); } return context; } @Override public WOResponse createResponseInContext(WOContext context) { WOResponse response = new ERXResponse(); return response; } /** * Override to perform any last minute cleanup before the application * terminates. See * {@class er.extensions.ERXGracefulShutdown ERXGracefulShutdown} for where * this is called if signal handling is enabled. Default implementation * calls terminate. */ public void gracefulTerminate() { terminate(); } /** * Logs the warning message if the main method was not called during the * startup. */ private void _displayMainMethodWarning() { log.warn("\n\nIt seems that your application class " + application().getClass().getName() + " did not call " + ERXApplication.class.getName() + ".main(argv[], applicationClass) method. " + "Please modify your Application.java as the followings so that " + ERXConfigurationManager.class.getName() + " can provide its " + "rapid turnaround feature completely. \n\n" + "Please change Application.java like this: \n" + "public static void main(String argv[]) { \n" + " ERXApplication.main(argv, Application.class); \n" + "}\n\n"); } /** improved streaming support */ protected NSMutableArray<String> _streamingRequestHandlerKeys = new NSMutableArray<String>(streamActionRequestHandlerKey()); public void registerStreamingRequestHandlerKey(String s) { if (!_streamingRequestHandlerKeys.containsObject(s)) { _streamingRequestHandlerKeys.addObject(s); } } public boolean isStreamingRequestHandlerKey(String s) { return _streamingRequestHandlerKeys.containsObject(s); } /** use the redirect feature */ protected Boolean _useSessionStoreDeadlockDetection; /** * Deadlock in session-store detection. Note that the detection only work in * single-threaded mode, and is mostly useful to find cases when a session * is checked out twice in a single RR-loop, which will lead to a session * store lockup. Set the * <code>er.extensions.ERXApplication.useSessionStoreDeadlockDetection=true</code> * property to actually the this feature. * * @return flag if to use the this feature */ public boolean useSessionStoreDeadlockDetection() { if (_useSessionStoreDeadlockDetection == null) { _useSessionStoreDeadlockDetection = ERXProperties.booleanForKey("er.extensions.ERXApplication.useSessionStoreDeadlockDetection") ? Boolean.TRUE : Boolean.FALSE; if (isConcurrentRequestHandlingEnabled() && _useSessionStoreDeadlockDetection.booleanValue()) { log.error("Sorry, useSessionStoreDeadlockDetection does not work with concurrent request handling enabled."); _useSessionStoreDeadlockDetection = Boolean.FALSE; } } return _useSessionStoreDeadlockDetection.booleanValue(); } /** * Returns true if this app is running in WO 5.4. * * @return true if this app is running in WO 5.4 */ public static boolean isWO54() { if (ERXApplication.isWO54 == null) { try { @SuppressWarnings("unused") Method getWebObjectsVersionMethod = WOApplication.class.getMethod("getWebObjectsVersion", new Class[0]); ERXApplication.isWO54 = Boolean.TRUE; } catch (Exception e) { ERXApplication.isWO54 = Boolean.FALSE; } } return ERXApplication.isWO54.booleanValue(); } /** * Returns whether or not this application is in development mode. This one * is named "Safe" because it does not require you to be running an * ERXApplication (and because you can't have a static and not-static method * of the same name. bah). If you are using ERXApplication, this will call * isDevelopmentMode on your application. If not, it will call * ERXApplication_defaultIsDevelopmentMode() which checks for the system * properties "er.extensions.ERXApplication.developmentMode" and/or "WOIDE". * * @return whether or not the current application is in development mode */ public static boolean isDevelopmentModeSafe() { boolean developmentMode; WOApplication application = WOApplication.application(); if (application instanceof ERXApplication) { ERXApplication erxApplication = (ERXApplication) application; developmentMode = erxApplication.isDevelopmentMode(); } else { developmentMode = ERXApplication._defaultIsDevelopmentMode(); } return developmentMode; } /** * Returns whether or not this application is running in development-mode. * If you are using Xcode, you should add a WOIDE=Xcode setting to your * launch parameters. */ protected static boolean _defaultIsDevelopmentMode() { boolean developmentMode = false; if (ERXProperties.stringForKey("er.extensions.ERXApplication.developmentMode") != null) { developmentMode = ERXProperties.booleanForKey("er.extensions.ERXApplication.developmentMode"); } else { String ide = ERXProperties.stringForKey("WOIDE"); if ("WOLips".equals(ide) || "Xcode".equals(ide)) { developmentMode = true; } } // AK: these are for quickly uncommenting while testing // if(true) return false; // if(true) return true; return developmentMode; } /** * Returns whether or not this application is running in development-mode. * If you are using Xcode, you should add a WOIDE=Xcode setting to your * launch parameters. */ public boolean isDevelopmentMode() { return ERXApplication._defaultIsDevelopmentMode(); } /** holds the info on checked-out sessions */ private Map<String, SessionInfo> _sessions = new HashMap<String, SessionInfo>(); /** Holds info about where and who checked out */ private class SessionInfo { Exception _trace = new Exception(); WOContext _context; public SessionInfo(WOContext wocontext) { _context = wocontext; } public Exception trace() { return _trace; } public WOContext context() { return _context; } public String exceptionMessageForCheckout(WOContext wocontext) { String contextDescription = null; if (_context != null) { contextDescription = "contextId: " + _context.contextID() + " request: " + _context.request(); } else { contextDescription = "<NULL>"; } log.error("There is an error in the session check-out: old context: " + contextDescription, trace()); if (_context == null) { return "Original context was null"; } else if (_context.equals(wocontext)) { return "Same context did check out twice"; } else { return "Context with id '" + wocontext.contextID() + "' did check out again"; } } } /** Overridden to check the sessions */ @Override public WOSession createSessionForRequest(WORequest worequest) { WOSession wosession = super.createSessionForRequest(worequest); if (useSessionStoreDeadlockDetection()) { _sessions.put(wosession.sessionID(), new SessionInfo(null)); } return wosession; } /** Overridden to check the sessions */ @Override public void saveSessionForContext(WOContext wocontext) { if (useSessionStoreDeadlockDetection()) { WOSession wosession = wocontext._session(); if (wosession != null) { String sessionID = wosession.sessionID(); SessionInfo sessionInfo = _sessions.get(sessionID); if (sessionInfo == null) { log.error("Check-In of session that was not checked out, most likely diue to an exception in session.awake(): " + sessionID); } else { _sessions.remove(sessionID); } } } super.saveSessionForContext(wocontext); } /** Overridden to check the sessions */ @Override public WOSession restoreSessionWithID(String sessionID, WOContext wocontext) { WOSession session = null; if (useSessionStoreDeadlockDetection()) { SessionInfo sessionInfo = _sessions.get(sessionID); if (sessionInfo != null) { throw new IllegalStateException(sessionInfo.exceptionMessageForCheckout(wocontext)); } session = super.restoreSessionWithID(sessionID, wocontext); if (session != null) { _sessions.put(session.sessionID(), new SessionInfo(wocontext)); } } else { session = super.restoreSessionWithID(sessionID, wocontext); } return session; } public Number sessionTimeOutInMinutes() { return new Integer(sessionTimeOut().intValue() / 60); } protected static final ERXFormatterFactory _formatterFactory = new ERXFormatterFactory(); /** * Getting formatters into KVC: bind to * <code>application.formatterFactory.(60/#,##0.00)</code> */ public ERXFormatterFactory formatterFactory() { return _formatterFactory; } protected Boolean _responseCompressionEnabled; /** * checks the value of * <code>er.extensions.ERXApplication.responseCompressionEnabled</code> * and if true turns on response compression by gzip */ public boolean responseCompressionEnabled() { if (_responseCompressionEnabled == null) { _responseCompressionEnabled = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.responseCompressionEnabled", false) ? Boolean.TRUE : Boolean.FALSE; } return _responseCompressionEnabled.booleanValue(); } /** * Returns an ERXMigrator with the lock owner name "appname-instancenumber". */ public ERXMigrator migrator() { return new ERXMigrator(name() + "-" + number()); } /** * This method is called by ERXWOContext and provides the application a hook * to rewrite generated URLs. * * You can also set "er.extensions.replaceApplicationPath.pattern" to the pattern to * match and "er.extensions.replaceApplicationPath.replace" to the value to replace * it with. * * For example, in Properties: * <code> * er.extensions.ERXApplication.replaceApplicationPath.pattern=/cgi-bin/WebObjects/YourApp.woa * er.extensions.ERXApplication.replaceApplicationPath.replace=/yourapp * </code> * * and in Apache 2.2: * <code> * RewriteRule ^/yourapp(.*)$ /cgi-bin/WebObjects/YourApp.woa$1 [PT,L] * </code> * * or Apache 1.3: * <code> * RewriteRule ^/yourapp(.*)$ /cgi-bin/WebObjects/YourApp.woa$1 [P,L] * </code> * * @param url * the URL to rewrite * @return the rewritten URL */ public String _rewriteURL(String url) { String processedURL = url; if (url != null && replaceApplicationPathPattern != null && replaceApplicationPathReplace != null) { processedURL = processedURL.replaceFirst(replaceApplicationPathPattern, replaceApplicationPathReplace); } return processedURL; } /** * Returns whether or not to rewrite direct connect URLs. * * @return whether or not to rewrite direct connect URLs */ public boolean rewriteDirectConnectURL() { return isDirectConnectEnabled() && !isCachingEnabled() && isDevelopmentMode() && ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.rewriteDirectConnect", false); } /** * Returns the directConnecURL, optionally rewritten. */ @Override public String directConnectURL() { String directConnectURL = super.directConnectURL(); if (rewriteDirectConnectURL()) { directConnectURL = _rewriteURL(directConnectURL); } return directConnectURL; } /** * Set the default encoding of the app (message encodings) * * @param encoding */ public void setDefaultEncoding(String encoding) { WOMessage.setDefaultEncoding(encoding); WOMessage.setDefaultURLEncoding(encoding); ERXMessageEncoding.setDefaultEncoding(encoding); ERXMessageEncoding.setDefaultEncodingForAllLanguages(encoding); } /** * Returns the component for the given class without having to cast. For * example: MyPage page = * ERXApplication.erxApplication().pageWithName(MyPage.class, context); * * @param <T> * the type of component to * @param componentClass * the component class to lookup * @param context * the context * @return the created component */ @SuppressWarnings("unchecked") public <T extends WOComponent> T pageWithName(Class<T> componentClass, WOContext context) { return (T) super.pageWithName(componentClass.getName(), context); } /** * Calls pageWithName with ERXWOContext.currentContext() for the current * thread. * * @param <T> * the type of component to * @param componentClass * the component class to lookup * @return the created component */ @SuppressWarnings("unchecked") public <T extends WOComponent> T pageWithName(Class<T> componentClass) { return (T)pageWithName(componentClass.getName(), ERXWOContext.currentContext()); } /** * Makes ERXConstants available for binding in the UI. Bind to <code>application.constants.MyConstantClass</code>. * @return */ public NSKeyValueCodingAdditions constants() { return new NSKeyValueCodingAdditions() { public void takeValueForKey(Object value, String key) { throw new IllegalArgumentException("Can't set constant"); } public Object valueForKey(String key) { return ERXConstant.constantsForClassName(key); } public void takeValueForKeyPath(Object value, String keyPath) { throw new IllegalArgumentException("Can't set constant"); } public Object valueForKeyPath(String keyPath) { return valueForKey(keyPath); } }; } }