package com.anjlab.ping.services; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.cache.Cache; import javax.cache.CacheException; import javax.cache.CacheFactory; import javax.cache.CacheManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.servlet.http.HttpServletResponse; import org.apache.tapestry5.EventContext; import org.apache.tapestry5.Link; import org.apache.tapestry5.MarkupWriter; import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.Translator; import org.apache.tapestry5.internal.jpa.EntityManagerSourceImpl; import org.apache.tapestry5.internal.services.ResourceStreamer; import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; import org.apache.tapestry5.ioc.Configuration; import org.apache.tapestry5.ioc.MappedConfiguration; import org.apache.tapestry5.ioc.MethodAdviceReceiver; import org.apache.tapestry5.ioc.OperationTracker; import org.apache.tapestry5.ioc.OrderedConfiguration; import org.apache.tapestry5.ioc.Resource; import org.apache.tapestry5.ioc.ServiceBinder; import org.apache.tapestry5.ioc.annotations.Contribute; import org.apache.tapestry5.ioc.annotations.InjectService; import org.apache.tapestry5.ioc.annotations.Match; import org.apache.tapestry5.ioc.annotations.Primary; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.ioc.services.PerthreadManager; import org.apache.tapestry5.ioc.services.ThreadCleanupListener; import org.apache.tapestry5.ioc.services.TypeCoercer; import org.apache.tapestry5.jpa.EntityManagerSource; import org.apache.tapestry5.jpa.JpaSymbols; import org.apache.tapestry5.jpa.JpaTransactionAdvisor; import org.apache.tapestry5.jpa.PersistenceUnitConfigurer; import org.apache.tapestry5.services.ApplicationStateManager; import org.apache.tapestry5.services.AssetPathConverter; import org.apache.tapestry5.services.ComponentEventLinkEncoder; import org.apache.tapestry5.services.Dispatcher; import org.apache.tapestry5.services.MarkupRenderer; import org.apache.tapestry5.services.MarkupRendererFilter; import org.apache.tapestry5.services.MetaDataLocator; import org.apache.tapestry5.services.PageRenderLinkSource; import org.apache.tapestry5.services.PageRenderRequestParameters; import org.apache.tapestry5.services.Request; import org.apache.tapestry5.services.RequestExceptionHandler; import org.apache.tapestry5.services.RequestFilter; import org.apache.tapestry5.services.RequestGlobals; import org.apache.tapestry5.services.RequestHandler; import org.apache.tapestry5.services.Response; import org.apache.tapestry5.services.ResponseCompressionAnalyzer; import org.apache.tapestry5.services.assets.StreamableResourceSource; import org.apache.tapestry5.services.linktransform.PageRenderLinkTransformer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.anjlab.gae.LocalMemorySoftCache; import com.anjlab.gae.QuotaDetails; import com.anjlab.ping.services.dao.AccountDAO; import com.anjlab.ping.services.dao.JobDAO; import com.anjlab.ping.services.dao.RefDAO; import com.anjlab.ping.services.dao.impl.cache.AccountDAOImplCache; import com.anjlab.ping.services.dao.impl.cache.JobDAOImplCache; import com.anjlab.ping.services.dao.impl.cache.RefDAOImplCache; import com.anjlab.ping.services.location.IPResolver; import com.anjlab.ping.services.location.LocationResolver; import com.anjlab.ping.services.location.TimeZoneResolver; import com.anjlab.ping.services.location.gae.GeonamesTimeZoneResolver; import com.anjlab.ping.services.location.gae.IPWhoisNetIPResolver; import com.anjlab.ping.services.location.gae.IPWhoisNetLocationResolver; import com.anjlab.ping.services.security.AccessController; import com.anjlab.tapestry5.StaticAssetPathConverter; import com.anjlab.tapestry5.StaticAssetResourceStreamer; import com.anjlab.tapestry5.TimeTranslator; import com.google.appengine.api.memcache.MemcacheService; import com.google.appengine.api.memcache.MemcacheServiceFactory; import com.google.appengine.api.memcache.stdimpl.GCacheFactory; import com.google.appengine.api.urlfetch.URLFetchServiceFactory; import com.google.appengine.api.utils.SystemProperty.Environment; import com.google.apphosting.api.ApiProxy.OverQuotaException; import com.google.apphosting.api.DeadlineExceededException; /** * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to * configure and extend Tapestry, or to place your own service definitions. */ public class AppModule { public static void bind(ServiceBinder binder) { binder.bind(JobExecutor.class).preventReloading().preventDecoration(); binder.bind(Mailer.class).preventReloading().preventDecoration(); binder.bind(AccountDAO.class, AccountDAOImplCache.class).preventReloading(); binder.bind(JobDAO.class, JobDAOImplCache.class).preventReloading(); binder.bind(RefDAO.class, RefDAOImplCache.class).preventReloading(); } public static AssetPathConverter decorateAssetPathConverter( final ResponseCompressionAnalyzer analyzer, final RequestGlobals requestGlobals) { return new StaticAssetPathConverter(requestGlobals); } @Contribute(PageRenderLinkTransformer.class) @Primary public static void provideURLRewriting( OrderedConfiguration<PageRenderLinkTransformer> configuration, final TypeCoercer typeCoercer) { configuration.add( "SupportOldUrls", new PageRenderLinkTransformer() { @Override public PageRenderRequestParameters decodePageRenderRequest(Request request) { String path = request.getPath(); if (path.startsWith("/job/analytics/1026/5002")) { return new PageRenderRequestParameters( "job/analytics", new EventContext() { @Override public String[] toStrings() { return new String[]{ "2865005" }; } @Override public int getCount() { return 1; } @SuppressWarnings("unchecked") @Override public <T> T get(Class<T> desiredType, int index) { if (desiredType == Long.class) { return (T) new Long("2865005"); } return null; } }, false); } return null; } @Override public Link transformPageRenderLink(Link defaultLink, PageRenderRequestParameters parameters) { return defaultLink; } }); } public static EntityManagerSource decorateEntityManagerSource(final Logger logger, @Symbol(JpaSymbols.PERSISTENCE_DESCRIPTOR) final Resource persistenceDescriptor, PersistenceUnitConfigurer packageNamePersistenceUnitConfigurer) { // XXX Waiting for https://issues.apache.org/jira/browse/TAP5-1848 return new EntityManagerSourceImpl(logger, persistenceDescriptor, packageNamePersistenceUnitConfigurer, new HashMap<String, PersistenceUnitConfigurer>()) { private Map<String, EntityManagerFactory> entityManagerFactories = getEntityManagerFactories(); @SuppressWarnings("unchecked") private Map<String, EntityManagerFactory> getEntityManagerFactories() { Field field = null; try { field = this.getClass().getSuperclass().getDeclaredField("entityManagerFactories"); field.setAccessible(true); return (Map<String, EntityManagerFactory>) field.get(this); } catch (Exception e) { throw new RuntimeException("Error accessing private field", e); } finally { if (field != null) field.setAccessible(false); } } @Override public EntityManagerFactory getEntityManagerFactory(String persistenceUnitName) { EntityManagerFactory emf = entityManagerFactories.get(persistenceUnitName); if (emf == null) { emf = Persistence.createEntityManagerFactory(persistenceUnitName); entityManagerFactories.put(persistenceUnitName, emf); } return emf; } }; } public static void contributeIgnoredPathsFilter(Configuration<String> configuration) { // GAE filters, except warmup requests configuration.add("^/_ah/((?!warmup).*)$"); // GAE Appstats configuration.add("^/appstats/.*$"); } public static Application buildApplication(AccountDAO accountDAO, JobDAO jobDAO, RefDAO refDAO, GAEHelper gaeHelper, JobExecutor jobExecutor, Mailer mailer, ApplicationStateManager stateManager, PageRenderLinkSource linkSource, RequestGlobals globals, MemcacheService memcache, TimeZoneResolver timeZoneResolver, LocationResolver locationResolver) { return new Application(accountDAO, jobDAO, refDAO, gaeHelper, jobExecutor, mailer, linkSource, globals, timeZoneResolver, locationResolver); } public static LocationResolver buildLocationResolver() { return new IPWhoisNetLocationResolver(URLFetchServiceFactory.getURLFetchService()); } public static IPResolver buildIPResolver() { return new IPWhoisNetIPResolver(URLFetchServiceFactory.getURLFetchService()); } public static TimeZoneResolver buildTimeZoneResolver() { return new GeonamesTimeZoneResolver(URLFetchServiceFactory.getURLFetchService(), "ping_service"); } public static Cache buildGAECache(Logger logger, PerthreadManager perthreadManager) { try { CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory(); Cache cache = cacheFactory.createCache(Collections.emptyMap()); final LocalMemorySoftCache cache2 = new LocalMemorySoftCache(cache); // perthreadManager may be null if we creating cache from AbstractFilter if (perthreadManager != null) { perthreadManager.addThreadCleanupListener(new ThreadCleanupListener() { @Override public void threadDidCleanup() { cache2.reset(); } }); } return cache2; } catch (CacheException e) { logger.error("Error instantiating cache", e); return null; } } public static MemcacheService buildMemcacheService(Logger logger) { MemcacheService memcacheService = MemcacheServiceFactory.getMemcacheService(); if (memcacheService == null) { logger.error("MemcacheService is null."); } return memcacheService; } public static GAEHelper buildGAEHelper(RequestGlobals requestGlobals) { return new GAEHelper(requestGlobals); } public static AccessController buildAccessController(GAEHelper helper, RequestGlobals globals) { return new AccessController(helper, globals); } public static Logger buildLogger() { return LoggerFactory.getLogger(AppModule.class); } public static void contributeApplicationDefaults(Logger logger, MappedConfiguration<String, String> configuration) { // Contributions to ApplicationDefaults will override any contributions to // FactoryDefaults (with the same key). Here we're restricting the supported // locales to just "en" (English). As you add localised message catalogs and other assets, // you can extend this list of locales (it's a comma separated series of locale names; // the first locale name is the default when there's no reasonable match). configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en"); // The factory default is true but during the early stages of an application // overriding to false is a good idea. In addition, this is often overridden // on the command line as -Dtapestry.production-mode=false boolean production = Environment.environment.value() == Environment.Value.Production; // Overriding for CreateStaticAssets // production = true; configuration.add(SymbolConstants.PRODUCTION_MODE, Boolean.toString(production)); configuration.add(SymbolConstants.COMPRESS_WHITESPACE, Boolean.toString(production)); // DataNucleus' GAE implementation doesn't provide EMF.getMetamodel() which is required for // providing entity value encoders configuration.add(JpaSymbols.PROVIDE_ENTITY_VALUE_ENCODERS, "false"); configuration.add(JpaSymbols.EARLY_START_UP, "false"); // Version should be changed when any resource (that is referenced from CSS) changes String version = "stage-20120311"; configuration.add(SymbolConstants.APPLICATION_VERSION, version); configuration.add(SymbolConstants.MINIFICATION_ENABLED, "true"); } /** * This is a service definition, the service will be named "TimingFilter". The interface, * RequestFilter, is used within the RequestHandler service pipeline, which is built from the * RequestHandler service configuration. Tapestry IoC is responsible for passing in an * appropriate Logger instance. Requests for static resources are handled at a higher level, so * this filter will only be invoked for Tapestry related requests. * * <p> * Service builder methods are useful when the implementation is inline as an inner class * (as here) or require some other kind of special initialization. In most cases, * use the static bind() method instead. * * <p> * If this method was named "build", then the service id would be taken from the * service interface and would be "RequestFilter". Since Tapestry already defines * a service named "RequestFilter" we use an explicit service id that we can reference * inside the contribution method. */ public RequestFilter buildTimingFilter(final Logger log, final Application application, final QuotaDetails quotaDetails) { return new RequestFilter() { public boolean service(Request request, Response response, RequestHandler handler) throws IOException { long startTime = System.currentTimeMillis(); try { if (!request.getPath().startsWith("/assets/") && !request.getPath().startsWith("/favicon.ico")) { try { if (!quotaDetails.isQuotaLimited()) { application.trackUserActivity(); } } catch (Exception e) { log.error("Error tracking user activity", e); quotaDetails.checkOverQuotaException(e); } } // The responsibility of a filter is to invoke the corresponding method // in the handler. When you chain multiple filters together, each filter // received a handler that is a bridge to the next filter. return handler.service(request, response); } finally { long elapsed = System.currentTimeMillis() - startTime; log.info(String.format("Request time [%s]: %d ms", request.getPath(), elapsed)); } } }; } public void contributeMasterDispatcher(OrderedConfiguration<Dispatcher> configuration, @InjectService("AccessController") Dispatcher accessController) { // TODO Investigate performance issue here configuration.add("AccessController", accessController, "before:PageRender"); } public static ResourceStreamer decorateResourceStreamer(Request request, Response response, StreamableResourceSource streamableResourceSource, ResponseCompressionAnalyzer analyzer, OperationTracker tracker, @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, ResourceChangeTracker resourceChangeTracker, ResourceStreamer streamer) { return new StaticAssetResourceStreamer( request, response, streamableResourceSource, analyzer, tracker, productionMode, resourceChangeTracker); } /** * This is a contribution to the RequestHandler service configuration. This is how we extend * Tapestry using the timing filter. A common use for this kind of filter is transaction * management or security. The @Local annotation selects the desired service by type, but only * from the same module. Without @Local, there would be an error due to the other service(s) * that implement RequestFilter (defined in other modules). */ public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration, @InjectService("TimingFilter") final RequestFilter timingFilter, @InjectService("Utf8Filter") final RequestFilter utf8Filter) { // Each contribution to an ordered configuration has a name, When necessary, you may // set constraints to precisely control the invocation order of the contributed filter // within the pipeline. configuration.add("Utf8Filter", utf8Filter); // handle UTF-8 configuration.add("Timing", timingFilter); } public static void contributeTranslatorSource(MappedConfiguration<Class<?>, Translator<Date>> configuration) { configuration.add(Date.class, new TimeTranslator()); } public static void contributeComponentMessagesSource(OrderedConfiguration<Resource> additionalBundles, TypeCoercer typeCoercer) { String path = TimeTranslator.class.getName().replace(".", "/") + ".properties"; final Resource resource = typeCoercer.coerce(path, Resource.class); if (resource.exists()) { additionalBundles.add("time-translator", resource); } else { // log or throw exceptions if you like } } public RequestFilter buildUtf8Filter( @InjectService("RequestGlobals") final RequestGlobals requestGlobals) { return new RequestFilter() { public boolean service(Request request, Response response, RequestHandler handler) throws IOException { requestGlobals.getHTTPServletRequest().setCharacterEncoding("UTF-8"); return handler.service(request, response); } }; } @Match("*DAO") public static void adviseTransactions(JpaTransactionAdvisor advisor, MethodAdviceReceiver receiver) { advisor.addTransactionCommitAdvice(receiver); } /* * Support pages without markup */ private static final String NO_MARKUP_SYMBOL = "NoMarkup"; public static final String NO_MARKUP = NO_MARKUP_SYMBOL + "=true"; public static void contributeFactoryDefaults(MappedConfiguration<String, String> configuration) { configuration.add(NO_MARKUP_SYMBOL, ""); } public void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration, final MetaDataLocator metaDataLocator, final ComponentEventLinkEncoder linkEncoder, final RequestGlobals globals) { configuration.add(NO_MARKUP_SYMBOL, new MarkupRendererFilter() { @Override public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) { PageRenderRequestParameters parameters = linkEncoder.decodePageRenderRequest(globals.getRequest()); boolean noMarkup = metaDataLocator.findMeta(NO_MARKUP_SYMBOL, parameters.getLogicalPageName(), Boolean.class); if (noMarkup) { // Provide default (empty) markup writer.element("html"); } else { renderer.renderMarkup(writer); } } }, "before:*"); } public static void contributeClasspathAssetAliasManager(MappedConfiguration<String, String> configuration) { configuration.add("cubics", "com/anjlab/cubics"); } public RequestExceptionHandler decorateRequestExceptionHandler( final Logger logger, final Response response, @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode) { if (!productionMode) return null; return new RequestExceptionHandler() { public void handleRequestException(Throwable exception) throws IOException { logger.error("Unexpected runtime exception", exception); if (Utils.isCause(exception, OverQuotaException.class)) { response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, null); } if (Utils.isCause(exception, DeadlineExceededException.class)) { response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, null); } else { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); } } }; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Match("IPResolver") public static void adviseCacheIPResolverMethods(final MethodAdviceReceiver receiver, Logger logger, PerthreadManager perthreadManager) { try { Map props = new HashMap(); // IP address of URL may change, keep it in cache for one day props.put(GCacheFactory.EXPIRATION_DELTA, 60 * 60 * 24); CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory(); Cache cache = cacheFactory.createCache(props); final LocalMemorySoftCache cache2 = new LocalMemorySoftCache(cache); // We don't want local memory cache live longer than Memcache // Since we don't have any mechanism to set local cache expiration // we will just reset this cache after each request perthreadManager.addThreadCleanupListener(new ThreadCleanupListener() { @Override public void threadDidCleanup() { cache2.reset(); } }); receiver.adviseAllMethods(new CacheMethodResultAdvice(IPResolver.class, cache2)); } catch (CacheException e) { logger.error("Error instantiating cache", e); } } @Match("LocationResolver") public static void adviseCacheLocationResolverMethods(final MethodAdviceReceiver receiver, Cache cache) { // Assume that location of IP address will never change, // so we don't have to set any custom cache expiration parameters receiver.adviseAllMethods(new CacheMethodResultAdvice(LocationResolver.class, cache)); } @Match("TimeZoneResolver") public static void adviseCacheTimeZoneResolverMethods(final MethodAdviceReceiver receiver, Cache cache) { // Assume that time zone of location will never change, // so we don't have to set any custom cache expiration parameters receiver.adviseAllMethods(new CacheMethodResultAdvice(LocationResolver.class, cache)); } public static QuotaDetails buildQuotaDetails(Cache cache, MemcacheService memcache) { return new QuotaDetails(cache, memcache); } // @Match("*") // public static void adviseProfiler(final MethodAdviceReceiver receiver) // { // final MethodAdvice advice = new ProfilingAdvice(receiver.getInterface().getName()); // // for (Method m : receiver.getInterface().getMethods()) { // receiver.adviseMethod(m, advice); // }; // } // // public static void contributeComponentClassTransformWorker( // OrderedConfiguration<ComponentClassTransformWorker> configuration, // ObjectLocator locator, // InjectionProvider injectionProvider, // ComponentClassResolver resolver) // { // configuration.add("ProfilerWorker", new ComponentClassTransformWorker() { // // @Override // public void transform(ClassTransformation transformation, final MutableComponentModel model) { // final MethodAdvice profilingAdvice = new ProfilingAdvice(transformation.getClassName()); // // for (TransformMethod method : transformation.matchMethods( // new Predicate<TransformMethod>() { // @Override // public boolean accept(TransformMethod method) { // return !method.getMethodIdentifier().contains("getComponentResources") // && !Modifier.isStatic(method.getSignature().getModifiers()) // && !Modifier.isAbstract(method.getSignature().getModifiers()); // } // })) // { // ComponentMethodAdvice advice = new ComponentMethodAdvice() // { // public void advise(ComponentMethodInvocation invocation) // { // profilingAdvice.advise(invocation); // } // }; // method.addAdvice(advice); // } // } // }, "before:Log"); // } }