/* * 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.FileReader; 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.BindException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.net.URLDecoder; import java.util.ArrayList; 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.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.lang3.CharEncoding; import org.apache.log4j.Appender; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.webobjects.appserver.WOAction; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOAdaptor; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOCookie; 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.appserver._private.WODeployedBundle; import com.webobjects.appserver._private.WOProperties; import com.webobjects.appserver._private.WOWebServicePatch; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOObserverCenter; 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.NSSelector; import com.webobjects.foundation.NSSet; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation.development.NSBundleFactory; 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.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.ERXMutableURL; import er.extensions.foundation.ERXPatcher; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXRuntimeUtilities; import er.extensions.foundation.ERXThreadStorage; import er.extensions.foundation.ERXTimestampUtilities; 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. * * 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. * * @property AppShouldExitOnOutOfMemoryError * @property ERApplicationName * @property ERApplicationNameSuffix * @property ERTimeToDie * @property ERTimeToKill * @property ERTimeToLive * @property NSProjectBundleEnabled * @property WOIDE * @property _DisableClasspathReorder * @property er.extensions.ERXApplication.DefaultEncoding * @property er.extensions.ERXApplication.DefaultMessageEncoding * @property er.extensions.ERXApplication.StatisticsBaseLogPath * @property er.extensions.ERXApplication.StatisticsLogRotationFrequency * @property er.extensions.ERXApplication.developmentMode * @property er.extensions.ERXApplication.enableERXShutdownHook * @property er.extensions.ERXApplication.fixCachingEnabled * @property er.extensions.ERXApplication.lowMemBufferSize * @property er.extensions.ERXApplication.memoryLowThreshold * @property er.extensions.ERXApplication.memoryStarvedThreshold * @property er.extensions.ERXApplication.memoryThreshold * @property er.extensions.ERXApplication.redirectOnMissingObjects * @property er.extensions.ERXApplication.replaceApplicationPath.pattern * @property er.extensions.ERXApplication.replaceApplicationPath.replace * @property er.extensions.ERXApplication.responseCompressionEnabled * @property er.extensions.ERXApplication.responseCompressionTypes * @property er.extensions.ERXApplication.rewriteDirectConnect * @property er.extensions.ERXApplication.ssl.enabled * @property er.extensions.ERXApplication.ssl.host * @property er.extensions.ERXApplication.ssl.port * @property er.extensions.ERXApplication.useSessionStoreDeadlockDetection * @property er.extensions.ERXComponentActionRedirector.enabled * @property er.extensions.ERXApplication.allowMultipleDevInstances */ public abstract class ERXApplication extends ERXAjaxApplication implements ERXGracefulShutdown.GracefulApplication { /** 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"); /** startup logging support */ public static final Logger startupLog = Logger.getLogger("er.extensions.ERXApplication.Startup"); private static boolean wasERXApplicationMainInvoked = false; /** empty array for adaptorExtensions */ private static String[] myAppExtensions = {}; /** * Notification to get posted when we get an OutOfMemoryError or when memory passes * the low memory threshold set in er.extensions.ERXApplication.memoryLowThreshold. * 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"; /** * Notification to get posted when we have recovered from a LowMemory condition. */ public static final String LowMemoryResolvedNotification = "LowMemoryResolvedNotification"; /** * Notification to get posted when we are on the brink of running out of memory. By * default, sessions will begin to be refused when this happens as well. */ public static final String StarvedMemoryNotification = "StarvedMemoryNotification"; /** * Notification to get posted when we have recovered from a StarvedMemory condition. */ public static final String StarvedMemoryResolvedNotification = "StarvedMemoryResolvedNotification"; /** * Notification to get posted when terminate() is called. */ public static final String ApplicationWillTerminateNotification = "ApplicationWillTerminateNotification"; /** * 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; /** * Property to control whether to exit on an OutOfMemoryError. */ public static final String AppShouldExitOnOutOfMemoryError = "er.extensions.AppShouldExitOnOutOfMemoryError"; /** * 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"; /** * Notification to post when all application initialization processes are complete (including migrations) */ public static final String ApplicationDidFinishInitializationNotification = "NSApplicationDidFinishInitializationNotification"; /** * 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<>(); protected 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.memoryStarvedThreshold */ protected BigDecimal _memoryStarvedThreshold; /** * Holds the value of the property * er.extensions.ERXApplication.memoryLowThreshold */ protected BigDecimal _memoryLowThreshold; /** * 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; /** * The SSL host used by this application. */ protected String _sslHost; /** * The SSL port used by this application. */ protected Integer _sslPort; /** * Tracks whether or not _addAdditionalAdaptors has been called yet. */ protected boolean _initializedAdaptors = false; /** * To support load balancing with mod_proxy */ private String _proxyBalancerRoute = null; /** * To support load balancing with mod_proxy */ private String _proxyBalancerCookieName = null; /** * To support load balancing with mod_proxy */ private String _proxyBalancerCookiePath = null; /** * 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).toURI().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 and ensuring all bundles are loaded * * @property er.extensions.appserver.projectBundleLoading - to see logging this has to be set on the command line by using -Der.extensions.appserver.projectBundleLoading=DEBUG * * @author ak */ public static class Loader { private JarChecker _checker; /** Holds the framework names during startup */ private Set<String> allFrameworks; private Properties allBundleProps; private Properties defaultProperties; private List<URL> allBundlePropURLs = new ArrayList<>(); private Properties readProperties(File file) { if (!file.exists()) { return null; } try { URL url = file.toURI().toURL(); return readProperties(url); } catch (MalformedURLException e) { e.printStackTrace(); return null; } } private Properties readProperties(URL url) { if (url == null) { return null; } try { Properties result = new Properties(); result.load(url.openStream()); urls.add(url); return result; } catch (MalformedURLException exception) { exception.printStackTrace(); return null; } catch (IOException exception) { return null; } } private Properties readProperties(NSBundle bundle, String name) { if (bundle == null) { return null; } if (name == null) { URL url = bundle.pathURLForResourcePath("Properties"); if(url != null) { urls.add(url); } return bundle.properties(); } try (InputStream inputStream = bundle.inputStreamForResourcePath(name)) { if(inputStream == null) { return null; } Properties result = new Properties(); result.load(inputStream); urls.add(bundle.pathURLForResourcePath(name)); return result; } catch (MalformedURLException exception) { exception.printStackTrace(); return null; } catch (IOException exception) { return null; } } /** * 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); defaultProperties = (Properties) NSProperties._getProperties().clone(); allFrameworks = new HashSet<>(); _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(); String projectPattern = ".*?/(\\w+)/bin$".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(); debugMsg("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 if (fixedJar.matches(projectPattern) || fixedJar.matches(".*?/erfoundation.jar") || fixedJar.matches(".*?/erwebobjects.jar")) { normalLibs += jar + File.pathSeparator; } else { jarLibs += jar + File.pathSeparator; } String bundle = jar.replaceAll(".*?[/\\\\](\\w+)\\.framework.*", "$1"); String 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); debugMsg("Added Real Bundle: " + bundle); } else { debugMsg("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); debugMsg("Added Jar bundle: " + bundle); } } // MS: This is totally hacked in to make Wonder startup properly with the new rapid turnaround. It's duplicating (poorly) // code from NSProjectBundle. I'm not sure we actually need this anymore, because NSBundle now fires an "all bundles loaded" event. else if (jar.endsWith("/bin") && new File(new File(jar).getParentFile(), ".project").exists()) { // AK: I have no idea if this is checked anywhere else, but this keeps is from having to set it in the VM args. debugMsg("Plain bundle: " + jar); for (File classpathFolder = new File(bundle); classpathFolder != null && classpathFolder.exists(); classpathFolder = classpathFolder.getParentFile()) { File projectFile = new File(classpathFolder, ".project"); if (projectFile.exists()) { try { boolean isBundle = false; Document projectDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(projectFile); projectDocument.normalize(); NodeList natureNodeList = projectDocument.getElementsByTagName("nature"); for (int natureNodeNum = 0; !isBundle && natureNodeNum < natureNodeList.getLength(); natureNodeNum ++) { Element natureContainerNode = (Element)natureNodeList.item(natureNodeNum); Node natureNode = natureContainerNode.getFirstChild(); String nodeValue = natureNode.getNodeValue(); // AK: we don't actually add apps to the bundle process (Mike, why not!?) if (nodeValue != null && nodeValue.startsWith("org.objectstyle.wolips.") && !nodeValue.contains("application")) { isBundle = true; } } if (isBundle) { System.setProperty("NSProjectBundleEnabled", "true"); String bundleName = classpathFolder.getName(); File buildPropertiesFile = new File(classpathFolder, "build.properties"); if (buildPropertiesFile.exists()) { Properties buildProperties = new Properties(); buildProperties.load(new FileReader(buildPropertiesFile)); if (buildProperties.get("project.name") != null) { // the project folder might be named differently than the actual bundle name bundleName = (String) buildProperties.get("project.name"); } } allFrameworks.add(bundleName); debugMsg("Added Binary Bundle (Project bundle): " + bundleName); } else { debugMsg("Skipping binary bundle: " + jar); } } catch (Throwable t) { System.err.println("Skipping '" + projectFile + "': " + t); } break; } debugMsg("Skipping, no project: " + projectFile); } } } 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); } if (System.getProperty("_DisableClasspathReorder") == null) { System.setProperty(cpName, newCP); } } } NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("bundleDidLoad", ERXConstant.NotificationClassArray), "NSBundleDidLoadNotification", null); } // for logging before logging has been setup and configured by loading the properties files private void debugMsg(String msg) { if ("DEBUG".equals(System.getProperty("er.extensions.appserver.projectBundleLoading"))) { System.out.println(msg); } } public boolean didLoad() { return (allFrameworks != null && allFrameworks.size() == 0); } private NSBundle mainBundle() { 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... if (isDevelopmentModeSafe() && ERXConfigurationManager.defaultManager().isDeployedAsServlet()) { // bundle-less builds do not appear to work when running in servlet mode, so make it prefer the legacy bundle style NSBundleFactory.registerBundleFactory(new com.webobjects.foundation.development.NSLegacyBundle.Factory()); } 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(); } return mainBundle; } /** * 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(); if (allFrameworks.contains(bundle.name())) { allFrameworks.remove(bundle.name()); debugMsg("Loaded " + bundle.name() + ". Remaining: " + allFrameworks); } else if (bundle.isFramework()) { debugMsg("Loaded unexpected framework bundle '" + bundle.name() + "'. Ensure your build.properties settings like project.name match the bundle name (including case)."); } if (allBundleProps == null) { allBundleProps = new Properties(); } String userName = propertyFromCommandLineFirst("user.name"); applyIfUnset(readProperties(bundle, "Properties." + userName)); applyIfUnset(readProperties(bundle, null)); if (allFrameworks.size() == 0) { mainProps = null; mainUserProps = null; collectMainProps(userName); allBundleProps.putAll(mainProps); if(mainUserProps!= null) { allBundleProps.putAll(mainUserProps); } String userHome = propertyFromCommandLineFirst("user.home"); Properties userHomeProps = null; if (userHome != null && userHome.length() > 0) { userHomeProps = readProperties(new File(userHome, "WebObjects.properties")); } if (userHomeProps != null) { allBundleProps.putAll(userHomeProps); } Properties props = NSProperties._getProperties(); props.putAll(allBundleProps); NSProperties._setProperties(props); insertCommandLineArguments(); if(userHomeProps != null) { urls.add(0,urls.remove(urls.size()-1)); } if(mainUserProps != null) { urls.add(0,urls.remove(urls.size()-1)); } urls.add(0,urls.remove(urls.size()-1)); // System.out.print(urls); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(AllBundlesLoadedNotification, NSKeyValueCoding.NullValue)); } } private List<URL> urls = new ArrayList<>(); private Properties mainProps; private Properties mainUserProps; private String propertyFromCommandLineFirst(String key) { String result = (String) propertiesFromArgv.valueForKey(key); if(result == null) { result = NSProperties.getProperty(key); } return result; } private void collectMainProps(String userName) { NSBundle mainBundle = mainBundle(); if (mainBundle != null) { mainUserProps = readProperties(mainBundle, "Properties." + userName); mainProps = readProperties(mainBundle, "Properties"); } if (mainProps == null) { String woUserDir = NSProperties.getProperty("webobjects.user.dir"); if (woUserDir == null) { woUserDir = System.getProperty("user.dir"); } mainUserProps = readProperties(new File(woUserDir, "Contents" + File.separator + "Resources" + File.separator + "Properties." + userName)); mainProps = readProperties(new File(woUserDir, "Contents" + File.separator + "Resources" + File.separator + "Properties")); } if (mainProps == null) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { Enumeration<URL> jarBundles = classLoader.getResources("Resources/Properties"); URL propertiesPath = null; URL userPropertiesPath = null; String mainBundleName = NSProperties._mainBundleName(); // Look for a jar file name like: myapp[-1.0][-SNAPSHOT].jar Pattern mainBundleJarPattern = Pattern.compile("\\b" + mainBundleName.toLowerCase() + "[-\\.\\d]*(snapshot)?\\.jar"); while (jarBundles.hasMoreElements()) { URL url = jarBundles.nextElement(); String urlAsString = url.toString(); if (mainBundleJarPattern.matcher(urlAsString.toLowerCase()).find()) { try { propertiesPath = new URL(URLDecoder.decode(urlAsString, CharEncoding.UTF_8)); userPropertiesPath = new URL(propertiesPath.toExternalForm() + userName); } catch (MalformedURLException exception) { exception.printStackTrace(); } catch (UnsupportedEncodingException exception) { exception.printStackTrace(); } break; } } mainProps = readProperties(propertiesPath); mainUserProps = readProperties(userPropertiesPath); } catch (IOException exception) { exception.printStackTrace(); } } 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."); } } private void applyIfUnset(Properties bundleProps) { if(bundleProps == null) return; 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()); } } } 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; InputStream is = null; 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) { 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(), CharEncoding.UTF_8); return content; } return null; } catch (FileNotFoundException e1) { return null; } catch (IOException e1) { throw NSForwardException._runtimeExceptionForThrowable(e1); } finally { if (is != null) { try { is.close(); } catch (IOException e) { // ignore } } } } } // You should not use ERXShutdownHook when deploying as servlet. protected static boolean enableERXShutdownHook() { return ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.enableERXShutdownHook", true); } /** * 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); if(enableERXShutdownHook()) { ERXShutdownHook.initERXShutdownHook(); } WOApplication.main(argv, applicationClass); } /** * <p>Terminates a different instance of the same application that may already be running.<br> * Only in dev mode.</p> * <p>Set the property "er.extensions.ERXApplication.allowMultipleDevInstances" to "true" if * you need to run multiple instances in dev mode.</p> * * @return true if a previously running instance was stopped. */ private static boolean stopPreviousDevInstance() { if (!isDevelopmentModeSafe() || ERXProperties.booleanForKeyWithDefault("er.extensions.ERXApplication.allowMultipleDevInstances", false)) { return false; } if (!(application().wasMainInvoked())) { return false; } try { ERXMutableURL adapterUrl = new ERXMutableURL(application().cgiAdaptorURL()); if (application().host() == null) { adapterUrl.setHost("localhost"); } adapterUrl.appendPath(application().name() + application().applicationExtension()); if (application().isDirectConnectEnabled()) { adapterUrl.setPort((Integer) application().port()); } else { adapterUrl.appendPath("-" + application().port()); } adapterUrl.appendPath(application().directActionRequestHandlerKey() + "/stop"); URL url = adapterUrl.toURL(); log.debug("Stopping previously running instance of " + application().name()); URLConnection connection = url.openConnection(); connection.getContent(); Thread.sleep(2000); return true; } catch (Throwable e) { e.printStackTrace(); } return false; } /** * 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 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) { if (other != null && other instanceof Entry) { return ((Entry) other).size() == size(); } return false; } @Override public int hashCode() { return (int) _size; } @Override public String toString() { return size() + "->" + jar(); } } private NSMutableDictionary<String, NSMutableArray<String>> packages = new NSMutableDictionary<>(); private NSMutableDictionary<String, NSMutableSet<Entry>> classes = new NSMutableDictionary<>(); private void processJar(String jar) { File jarFile = new File(jar); if (!jarFile.exists() || jarFile.isDirectory()) { return; } try (JarFile f = new JarFile(jar)) { for (Enumeration<JarEntry> enumerator = f.entries(); enumerator.hasMoreElements();) { JarEntry entry = 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<>(); 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<>(); classes.setObjectForKey(set, name); } set.addObject(e); } } } catch (IOException e) { startupLog.error("Error in processing jar: "+ jar, e); } } private void reportErrors() { StringBuilder sb = new StringBuilder(); 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) { startupLog.debug("The following packages appear multiple times:\n" + message); } sb = new StringBuilder(); NSMutableSet<String> classPackages = new NSMutableSet<>(); 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) { startupLog.debug("The following packages have different versions, you should remove the version you don't want:\n" + message); } } } /** * This heuristic to determine if an application is deployed as servlet relays on the fact, * that contextClassName() is set WOServletContext or ERXWOServletContext * * @return true if the application is deployed as servlet. */ public boolean isDeployedAsServlet() { return contextClassName().contains("Servlet"); // i.e one of WOServletContext or ERXWOServletContext } /** * Called prior to actually initializing the app. Defines framework load * order, class path order, checks patches etc. */ public static void setup(String[] argv) { _loader = new Loader(argv); if (System.getProperty("_DisableClasspathReorder") == null) { ClassLoader loader = AppClassLoader.getAppClassLoader(); Thread.currentThread().setContextClassLoader(loader); } ERXConfigurationManager.defaultManager().setCommandLineArguments(argv); ERXFrameworkPrincipal.setUpFrameworkPrincipalClass(ERXExtensions.class); // NSPropertiesCoordinator.loadProperties(); if(enableERXShutdownHook()) { ERXShutdownHook.useMe(); } } /** * Installs several bugfixes 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")) { setContextClassName(ERXWOContext.class.getName()); } if (contextClassName().equals("WOServletContext") || contextClassName().equals("com.webobjects.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"); } @Override public WOResourceManager createResourceManager() { return new ERXResourceManager(); } /** * The ERXApplication constructor. */ public ERXApplication() { super(); /* * ERXComponentRequestHandler is a patched version of the original WOComponentRequestHandler * This method will tell Application to used the patched, the patched version will disallow direct component access by name * If you want to use the unpatched version set the property ERXDirectComponentAccessAllowed to true */ if (!ERXProperties.booleanForKeyWithDefault("ERXDirectComponentAccessAllowed", false)) { ERXComponentRequestHandler erxComponentRequestHandler = new ERXComponentRequestHandler(); registerRequestHandler(erxComponentRequestHandler, componentRequestHandlerKey()); } ERXStats.initStatisticsIfNecessary(); try { WOWebServicePatch.initServer(); } catch (Throwable e) { Throwable cause = ERXExceptionUtilities.getMeaningfulThrowable(e); if (!(cause instanceof ClassNotFoundException || cause instanceof NoClassDefFoundError)) { e.printStackTrace(); } } // WOFrameworksBaseURL and WOApplicationBaseURL properties are broken in 5.4. // This is the workaround. frameworksBaseURL(); applicationBaseURL(); if (System.getProperty("WOFrameworksBaseURL") != null) { setFrameworksBaseURL(System.getProperty("WOFrameworksBaseURL")); } if (System.getProperty("WOApplicationBaseURL") != null) { setApplicationBaseURL(System.getProperty("WOApplicationBaseURL")); } if (!ERXConfigurationManager.defaultManager().isDeployedAsServlet() && (!wasERXApplicationMainInvoked || _loader == null)) { _displayMainMethodWarning(); } // try { // NSBundle.mainBundle().versionString(); // } catch (NoSuchMethodError e) { // throw new RuntimeException("No versionString() method in NSBundle found. \nThis means your class path is incorrect. Adjust it so that ERJars comes before JavaFoundation."); // } if (_loader == null) { System.out.println("No loader: " + System.getProperty("java.class.path")); } else if (!_loader.didLoad()) { throw new RuntimeException("ERXExtensions have not been initialized. Debugging information can be enabled by adding the JVM argument: '-Der.extensions.appserver.projectBundleLoading=DEBUG'. Please report the classpath and the rest of the bundles to the Wonder mailing list: " + "\nRemaining frameworks: " + (_loader == null ? "none" : _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(); } } if(_loader != null) { _loader._checker.reportErrors(); _loader._checker = null; } didCreateApplication(); 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 (_rapidTurnaroundActiveForAnyProject() && 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); } log.info("Wonder version: " + ERXProperties.wonderVersion()); // Configure the WOStatistics CLFF logging since it can't be controlled // 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); NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("addBalancerRouteCookieByNotification", new Class[] { NSNotification.class }), WORequestHandler.DidHandleRequestNotification, null); // 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()); _memoryStarvedThreshold = ERXProperties.bigDecimalForKey("er.extensions.ERXApplication.memoryThreshold"); // MS: Kept around for backwards compat, replaced by memoryStarvedThreshold now _memoryStarvedThreshold = ERXProperties.bigDecimalForKeyWithDefault("er.extensions.ERXApplication.memoryStarvedThreshold", _memoryStarvedThreshold); _memoryLowThreshold = ERXProperties.bigDecimalForKeyWithDefault("er.extensions.ERXApplication.memoryLowThreshold", _memoryLowThreshold); _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() + applicationExtension(); if (_replaceApplicationPathReplace == null) { _replaceApplicationPathReplace = ""; } } } /** * Called, for example, when refuse new sessions is enabled and the request contains an expired session. * If mod_rewrite is being used we don't want the adaptor prefix being part of the redirect. * @see com.webobjects.appserver.WOApplication#_newLocationForRequest(com.webobjects.appserver.WORequest) */ @Override public String _newLocationForRequest(WORequest aRequest) { return _rewriteURL(super._newLocationForRequest(aRequest)); } /** * 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 subclasses' {@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()) { ERXMigrator migrator = migrator(); migrationsWillRun(migrator); migrator.migrateToLatest(); migrationsDidRun(migrator); } NSNotificationCenter.defaultCenter().postNotification(new NSNotification(ERXApplication.ApplicationDidFinishInitializationNotification, this)); } /** * Called prior to migrations running. * @param migrator the migrator that will be used */ protected void migrationsWillRun(ERXMigrator migrator) { // DO NOTHING } /** * Called after migrations finish running. * @param migrator the migrator that was used */ protected void migrationsDidRun(ERXMigrator migrator) { // DO NOTHING } /** * 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 } protected void didCreateApplication() { // 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() { } /** * 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: * <p> * 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>. * <p>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. */ @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 += Math.random() * 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 - ERXTimestampUtilities.hourOfDay(now)) * 3600 - ERXTimestampUtilities.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 += (Math.random() * 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; } } @Override public WORequest createRequest(String aMethod, String aURL, String anHTTPVersion, Map<String, ? extends List<String>> someHeaders, NSData aContent, Map<String, Object> 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.get("content-type") != null) { someHeaders.remove("content-type"); } if (rewriteDirectConnectURL()) { aURL = adaptorPath() + name() + applicationExtension() + aURL; } return new ERXRequest(aMethod, aURL, anHTTPVersion, someHeaders, aContent, someInfo); } /** * 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 */ @Override 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, (nsarray !=null ? nsarray.arrayByAddingObjectsFromArray(_expectedLanguages()) : _expectedLanguages())); } return super._componentDefinition(s, nsarray); } private boolean _isMemoryLow = false; private boolean _isMemoryStarved = false; /** * <p> * Checks if the free memory is less than the threshold given in * <code>er.extensions.ERXApplication.memoryStarvedThreshold</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. * </p> * * <p> * Additionally, you can set <code>er.extensions.ERXApplication.memoryLowThreshold</code>, which * you can set at a higher "warning" level, before the situation is critical. * </p> * * <p> * Both of these methods post notifications both at the start of the event as well as the end * of the event (LowMemoryNotification/LowMemoryResolvedNotification and StarvedMemoryNotification * and StarvedMemoryResolvedNotification). * </p> * * @author ak */ protected void checkMemory() { boolean memoryLow = checkMemory(_memoryLowThreshold, false); if(memoryLow != _isMemoryLow) { if(!memoryLow) { log.warn("App is no longer low on memory"); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(LowMemoryResolvedNotification, this)); } else { log.error("App is low on memory"); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(LowMemoryNotification, this)); } _isMemoryLow = memoryLow; } boolean memoryStarved = checkMemory(_memoryStarvedThreshold, true); if(memoryStarved != _isMemoryStarved) { if(!memoryStarved) { log.warn("App is no longer starved, handling new sessions again"); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(StarvedMemoryResolvedNotification, this)); } else { log.error("App is starved, starting to refuse new sessions"); NSNotificationCenter.defaultCenter().postNotification(new NSNotification(StarvedMemoryNotification, this)); } _isMemoryStarved = memoryStarved; } } protected boolean checkMemory(BigDecimal memoryThreshold, boolean attemptGC) { boolean pastThreshold = false; 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 starvedThreshold = (long) (memoryThreshold.doubleValue() < 1.0 ? memoryThreshold.doubleValue() * max : (max - (memoryThreshold.doubleValue() * 1024 * 1024))); synchronized (this) { long time = System.currentTimeMillis(); if (attemptGC && (used > starvedThreshold) && (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; } pastThreshold = (used > starvedThreshold); } } return pastThreshold; } /** * Override and return false if you do not want sessions to be refused when memory is starved. * * @return whether or not sessions should be refused on starved memory */ protected boolean refuseSessionsOnStarvedMemory() { return true; } /** * Overridden to return the super value OR true if the app is memory starved. */ @Override public boolean isRefusingNewSessions() { return super.isRefusingNewSessions() || (refuseSessionsOnStarvedMemory() && _isMemoryStarved); } /** * Overridden to fix that direct connect apps can't refuse new sessions. */ @Override public synchronized void refuseNewSessions(boolean value) { boolean success = false; try { Field f = WOApplication.class.getDeclaredField("_refusingNewClients"); f.setAccessible(true); f.set(this, value); success = true; } catch (SecurityException e) { log.error(e, e); } catch (NoSuchFieldException e) { log.error(e, e); } catch (IllegalArgumentException e) { log.error(e, e); } catch (IllegalAccessException e) { log.error(e, e); } if(!success) { super.refuseNewSessions(value); } // #81712. App will terminate immediately if the right conditions are met. if (value && (activeSessionsCount() <= minimumActiveSessionsCount())) { log.info("Refusing new clients and below min active session threshold, about to terminate..."); terminate(); } 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. * <p> * The name suffix is set via the System property <b>ERApplicationNameSuffix</b>. * 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. * * @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. * * @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. * * @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: * <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. * * @param e exception * @param context the current context * @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 * overridden 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. // CHECKME this was created for WO 5.2, do we still need this for 5.4.3? @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; boolean didCreateContext = false; if(context == null) { // AK: we provide the "handleException" with not much enough info to output a reasonable error message context = createContextForRequest(aRequest); didCreateContext = true; } WOResponse response = handleException(exception, context); // CH: If we have created a context, then the request handler won't know about it and can't put the components // from handleException(exception, context) to sleep nor check-in any session that may have been checked out // or created (e.g. from a component action URL. // // I'm not sure if the reasoning below was valid, or of the real cause of this deadlocking was creating the context // above and then creating / checking out a session during handleException(exception, context). In any case, a zombie // session was getting created with WO 5.4.3 and this does NOT happen with a pure WO application making the code above // a prime suspect. I am leaving the code below in so that if it does something for prior versions, that will still work. if (didCreateContext) { context._putAwakeComponentsToSleep(); saveSessionForContext(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. else if (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) { boolean shouldExitOnOOMError = ERXProperties.booleanForKeyWithDefault(AppShouldExitOnOutOfMemoryError, true); shouldQuit = shouldExitOnOOMError; // 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"); log.error("Ran out of memory, sending notification to clear caches", throwable); 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 = shouldExitOnOOMError; } } // 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.fatal("Ran out of memory, killing this instance"); log.fatal("Ran out of memory, killing this instance", throwable); } } else { // We log just in case the log4j call puts us in a bad // state. NSLog.err.appendln("java.lang.Error \"" + throwable.getClass().getName() + "\" occured."); log.error("java.lang.Error \"" + throwable.getClass().getName() + "\" occured.", 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(); /* * Clear the _ThreadInfo in the EOObserverCenter for this thread to prevent a bug * which results in ECs loosing track of change state on multiple worker threads. * A more complete explanation available here: * http://www.mail-archive.com/webobjects-dev@lists.apple.com/msg25391.html */ EOObserverCenter.notifyObserversObjectWillChange(null); // 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. */ 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 */ 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, "key"); 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/") || responseCompressionTypes().containsObject(contentType))) { String acceptEncoding = request.headerForKey("accept-encoding"); if ((acceptEncoding != null) && (acceptEncoding.toLowerCase().indexOf("gzip") != -1)) { long start = System.currentTimeMillis(); long inputBytesLength; InputStream contentInputStream = response.contentInputStream(); NSData compressedData; if (contentInputStream != null) { inputBytesLength = response.contentInputStreamLength(); compressedData = ERXCompressionUtilities.gzipInputStreamAsNSData(contentInputStream, (int)inputBytesLength); response.setContentStream(null, 0, 0); } else { NSData input = response.content(); inputBytesLength = input.length(); if(inputBytesLength > 0) { compressedData = ERXCompressionUtilities.gzipByteArrayAsNSData(input._bytesNoCopy(), 0, (int)inputBytesLength); } else { compressedData = NSData.EmptyData; } } if ( inputBytesLength > 0 ) { if (compressedData == null) { // something went wrong } else { response.setContent(compressedData); 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(context); return response; } /** * Override to perform any last minute cleanup before the application * terminates. See * {@link er.extensions.components.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<>(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 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; } if (!developmentMode) { developmentMode = ERXProperties.booleanForKey("NSProjectBundleEnabled"); } } // 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. * @return <code>true</code> if application is in dev mode */ public boolean isDevelopmentMode() { return ERXApplication._defaultIsDevelopmentMode(); } /** holds the info on checked-out sessions */ private Map<String, SessionInfo> _sessions = new HashMap<>(); /** 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 (wosession != null && 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) { if(sessionID != null && ERXSession.session() != null && sessionID.equals(ERXSession.session().sessionID())) { // AK: I have no idea how this can happen throw new IllegalStateException("Trying to check out a session twice in one RR loop: " + sessionID); } 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 Integer.valueOf(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(); } protected NSSet<String> _responseCompressionTypes; /** * checks the value of * <code>er.extensions.ERXApplication.responseCompressionTypes</code> for * mime types that allow response compression in addition to text/* types. * The default is ("application/x-javascript") * @return an array of mime type strings */ public NSSet<String> responseCompressionTypes() { if(_responseCompressionTypes == null) { _responseCompressionTypes = new NSSet<>(ERXProperties.arrayForKeyWithDefault("er.extensions.ERXApplication.responseCompressionTypes", new NSArray<String>("application/x-javascript"))); } return _responseCompressionTypes; } /** * Returns an ERXMigrator with the lock owner name "appname-instancenumber". * @return migrator for this instance */ public ERXMigrator migrator() { return new ERXMigrator(name() + "-" + host() + ":" + port()); } /** * 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; } /** * This method is called by ERXResourceManager and provides the application a hook * to rewrite generated URLs for resources. * * @param url * the URL to rewrite * @param bundle * the bundle the resource is located in * @return the rewritten URL */ public String _rewriteResourceURL(String url, WODeployedBundle bundle) { return url; } /** * 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>. */ 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); } }; } /** * Returns whether or not DirectConnect SSL should be enabled. If you set this, please * review the DirectConnect SSL section of the ERExtensions sample Properties file to * learn more about how to properly configure it. * * @return whether or not DirectConnect SSL should be enabled * @property er.extensions.ERXApplication.ssl.enabled */ public boolean sslEnabled() { return ERXProperties.booleanForKey("er.extensions.ERXApplication.ssl.enabled"); } /** * Returns the host name that will be used to bind the SSL socket to (defaults to host()). * * @return the SSL socket host * @property er.extensions.ERXApplication.ssl.host */ public String sslHost() { String sslHost = _sslHost; if (sslHost == null) { sslHost = ERXProperties.stringForKeyWithDefault("er.extensions.ERXApplication.ssl.host", host()); } return sslHost; } /** * Sets an SSL host override. * * @param sslHost an SSL host override */ public void _setSslHost(String sslHost) { _sslHost = sslHost; } /** * Returns the SSL port that will be used for DirectConnect SSL (defaults to 443). A value of * 0 will cause WO to autogenerate an SSL port number. * * @return the SSL port that will be used for DirectConnect SSL * @property er.extensions.ERXApplication.ssl.port */ public int sslPort() { int sslPort; if (_sslPort != null) { sslPort = _sslPort.intValue(); } else { sslPort = ERXProperties.intForKeyWithDefault("er.extensions.ERXApplication.ssl.port", 443); } return sslPort; } /** * Sets an SSL port override (called back by the ERXSecureAdaptor) * * @param sslPort an ssl port override */ public void _setSslPort(int sslPort) { _sslPort = sslPort; } /** * Injects additional adaptors into the WOAdditionalAdaptors setting. Subclasses can extend this * method, but should call super._addAdditionalAdaptors. * * @param additionalAdaptors the mutable adaptors array */ protected void _addAdditionalAdaptors(NSMutableArray<NSDictionary<String, Object>> additionalAdaptors) { if (sslEnabled()) { boolean sslAdaptorConfigured = false; for (NSDictionary<String, Object> adaptor : additionalAdaptors) { if (ERXSecureDefaultAdaptor.class.getName().equals(adaptor.objectForKey(WOProperties._AdaptorKey))) { sslAdaptorConfigured = true; } } ERXSecureDefaultAdaptor.checkSSLConfig(); if (!sslAdaptorConfigured) { NSMutableDictionary<String, Object> sslAdaptor = new NSMutableDictionary<>(); sslAdaptor.setObjectForKey(ERXSecureDefaultAdaptor.class.getName(), WOProperties._AdaptorKey); String sslHost = sslHost(); if (sslHost != null) { sslAdaptor.setObjectForKey(sslHost, WOProperties._HostKey); } sslAdaptor.setObjectForKey(Integer.valueOf(sslPort()), WOProperties._PortKey); additionalAdaptors.addObject(sslAdaptor); } } } /** * Returns the additionalAdaptors, but calls _addAdditionalAdaptors to give the runtime an opportunity to * programmatically force adaptors into the list. */ @Override @SuppressWarnings("deprecation") public NSArray<NSDictionary<String, Object>> additionalAdaptors() { NSArray<NSDictionary<String, Object>> additionalAdaptors = super.additionalAdaptors(); if (!_initializedAdaptors) { NSMutableArray<NSDictionary<String, Object>> mutableAdditionalAdaptors = additionalAdaptors.mutableClone(); _addAdditionalAdaptors(mutableAdditionalAdaptors); _initializedAdaptors = true; additionalAdaptors = mutableAdditionalAdaptors; setAdditionalAdaptors(mutableAdditionalAdaptors); } return additionalAdaptors; } @Override public WOAdaptor adaptorWithName(String aClassName, NSDictionary<String, Object> anArgsDictionary) { try { return super.adaptorWithName(aClassName, anArgsDictionary); } catch (NSForwardException e) { Throwable rootCause = ERXExceptionUtilities.getMeaningfulThrowable(e); if ((rootCause instanceof BindException) && stopPreviousDevInstance()) { return super.adaptorWithName(aClassName, anArgsDictionary); } throw e; } } protected void _debugValueForDeclarationNamed(WOComponent component, String verb, String aDeclarationName, String aDeclarationType, String aBindingName, String anAssociationDescription, Object aValue) { if (aValue instanceof String) { StringBuilder stringbuffer = new StringBuilder(((String) aValue).length() + 2); stringbuffer.append('"'); stringbuffer.append(aValue); stringbuffer.append('"'); aValue = stringbuffer; } if (aDeclarationName.startsWith("_")) { aDeclarationName = "[inline]"; } StringBuilder sb = new StringBuilder(); //NSArray<WOComponent> componentPath = ERXWOContext._componentPath(ERXWOContext.currentContext()); //componentPath.lastObject() //WOComponent lastComponent = ERXWOContext.currentContext().component(); String lastComponentName = component.name().replaceFirst(".*\\.", ""); sb.append(lastComponentName); sb.append(verb); if (!aDeclarationName.startsWith("_")) { sb.append(aDeclarationName); sb.append(':'); } sb.append(aDeclarationType); sb.append(" { "); sb.append(aBindingName); sb.append('='); String valueStr = aValue != null ? aValue.toString() : "null"; if (anAssociationDescription.startsWith("class ")) { sb.append(valueStr); sb.append("; }"); } else { sb.append(anAssociationDescription); sb.append("; } value "); sb.append(valueStr); } NSLog.debug.appendln(sb.toString()); } /** * The set of component names that have binding debug enabled */ private NSMutableSet<String> _debugComponents = new NSMutableSet<>(); /** * Little bit better binding debug output than the original. */ @Override public void logTakeValueForDeclarationNamed(String aDeclarationName, String aDeclarationType, String aBindingName, String anAssociationDescription, Object aValue) { WOComponent component = ERXWOContext.currentContext().component(); if (component.parent() != null) { component = component.parent(); } _debugValueForDeclarationNamed(component, " ==> ", aDeclarationName, aDeclarationType, aBindingName, anAssociationDescription, aValue); } /** * Little bit better binding debug output than the original. */ @Override public void logSetValueForDeclarationNamed(String aDeclarationName, String aDeclarationType, String aBindingName, String anAssociationDescription, Object aValue) { WOComponent component = ERXWOContext.currentContext().component(); if (component.parent() != null) { component = component.parent(); } _debugValueForDeclarationNamed(component, " <== ", aDeclarationName, aDeclarationType, aBindingName, anAssociationDescription, aValue); } /** * Turns on/off binding debugging for the given component. Binding debugging requires using the WOOgnl * template parser and setting ognl.debugSupport=true. * * @param debugEnabled whether or not to enable debugging * @param componentName the component name to enable debugging for */ public void setDebugEnabledForComponent(boolean debugEnabled, String componentName) { if (debugEnabled) { _debugComponents.addObject(componentName); } else { _debugComponents.removeObject(componentName); } } /** * Returns whether or not binding debugging is enabled for the given component * * @param componentName the component name * @return whether or not binding debugging is enabled for the given component */ public boolean debugEnabledForComponent(String componentName) { return _debugComponents.containsObject(componentName); } /** * Turns off binding debugging for all components. */ public void clearDebugEnabledForAllComponents() { _debugComponents.removeAllObjects(); } /** * Workaround for method missing in 5.3. Misnamed because static methods can't override client methods. * @return the request handler key for ajax. * @deprecated use {@link #ajaxRequestHandlerKey()} instead */ @Deprecated public static String erAjaxRequestHandlerKey() { return "ja"; } /** * Sends out a ApplicationWillTerminateNotification before actually starting to terminate. */ @Override public void terminate() { NSNotificationCenter.defaultCenter().postNotification(ApplicationWillTerminateNotification, this); super.terminate(); } /** * Override default implementation that returns {".dll", ".exe"} and therefor prohibits IIS * as WebServer. */ @Override public String[] adaptorExtensions() { return myAppExtensions; } public void addBalancerRouteCookieByNotification(NSNotification notification) { if (notification.object() instanceof WOContext) { addBalancerRouteCookie((WOContext) notification.object()); } } public void addBalancerRouteCookie(WOContext context) { if (context != null && context.request() != null && context.response() != null) { if (_proxyBalancerRoute == null) { _proxyBalancerRoute = (name() + "_" + port().toString()).toLowerCase(); _proxyBalancerRoute = "." + _proxyBalancerRoute.replace('.', '_'); } if (_proxyBalancerCookieName == null) { _proxyBalancerCookieName = ("routeid_" + name()).toLowerCase(); _proxyBalancerCookieName = _proxyBalancerCookieName.replace('.', '_'); } if (_proxyBalancerCookiePath == null) { _proxyBalancerCookiePath = (System.getProperty("FixCookiePath") != null) ? System.getProperty("FixCookiePath") : "/"; } WOCookie cookie = new WOCookie(_proxyBalancerCookieName, _proxyBalancerRoute, _proxyBalancerCookiePath, null, -1, context.request().isSecure(), true); cookie.setExpires(null); context.response().addCookie(cookie); } } }