package org.wisdom.browserwatch; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import org.apache.felix.ipojo.annotations.Context; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; import org.osgi.framework.ServiceEvent; import org.osgi.framework.ServiceListener; import org.osgi.framework.ServiceReference; import org.osgi.service.log.LogService; import org.osgi.util.tracker.BundleTracker; import org.osgi.util.tracker.BundleTrackerCustomizer; import org.wisdom.api.DefaultController; import org.wisdom.api.annotations.Body; import org.wisdom.api.annotations.Closed; import org.wisdom.api.annotations.Controller; import org.wisdom.api.annotations.OnMessage; import org.wisdom.api.annotations.Opened; import org.wisdom.api.annotations.Parameter; import org.wisdom.api.annotations.Route; import org.wisdom.api.annotations.View; import org.wisdom.api.http.HttpMethod; import org.wisdom.api.http.Result; import org.wisdom.api.http.websockets.Publisher; import org.wisdom.api.router.Router; import org.wisdom.api.templates.Template; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; /** * Controller that will notify websocket listeners that main application bundle * got reloaded * * @author ndelsaux * */ @Controller public class BrowserWatchController extends DefaultController implements BundleTrackerCustomizer<BundleInfos>, ServiceListener { private static final String BROWSER_WATCH_BUNDLES = "/browserWatch/bundles"; private static final String BROWSER_WATCH_SOCKET = "/browserWatch/socket"; @Requires LogService log; /** * Router is used to associate currently displayed pages (as obtained by {@link #addedClient(String, String)}) * with bundles and their lifecycle. */ @Requires Router router; /** * This web-socket publisher allows browsers to receive various ntofications (most important being * the ones regarding services reload) */ @Requires private Publisher publisher; @Context BundleContext context; /** * Contains the collection of controllers clients are currently viewing, organized as a * map to make sure we correctly follow clients */ private BiMap<String, String> displayedControllers = HashBiMap.create(); @View("bundles") Template template; private BundleTracker<BundleInfos> tracker; private BiMap<Bundle, BundleInfos> bundleInfosMapping = HashBiMap.create(); /** * Simple test route showing all loaded bundles, and the route of the current one * @return */ @Route(method = HttpMethod.GET, uri = BROWSER_WATCH_BUNDLES) public Result bundles() { return ok(render(template)); } /** * When a client is added, we get the service it should be associated with using the route resoluton * mechanism wisdom provides us * @param client * @param url */ @Opened(BROWSER_WATCH_SOCKET) public void addedClient(@Parameter("client") String client) { log.log(LogService.LOG_INFO, String.format("added client %s", client)); } private org.wisdom.api.router.Route getRoute(String url) { for(HttpMethod m : HttpMethod.values()) { org.wisdom.api.router.Route r = router.getRouteFor(m, url); if(!r.isUnbound()) { log.log(LogService.LOG_INFO, String.format("Url %s is mapped to route %s", url, r)); return r; } } return null; } /** * And when client leaves, stop notifying it * * @param client * client to remove */ @Closed(BROWSER_WATCH_SOCKET) public void removedClient(@Parameter("client") String client) { displayedControllers.inverse().remove(client); } @OnMessage(BROWSER_WATCH_SOCKET) public void receiveClientIp(@Parameter("client") String client, @Body String url) { log.log(LogService.LOG_INFO, String.format("client %s is browsing URL %s", client, url)); org.wisdom.api.router.Route visitedRoute = getRoute(url); if(visitedRoute!=null) { log.log(LogService.LOG_INFO, String.format("The url %s is mapped on route %s", url, visitedRoute)); displayedControllers.put(visitedRoute.getControllerClass().getName(), client); } } @Validate public void listenServices() { tracker = new BundleTracker<BundleInfos>(context, Bundle.ACTIVE, this); tracker.open(); context.addServiceListener(this); } @Invalidate public void stopListening() { tracker.close(); context.removeServiceListener(this); } public BundleInfos addingBundle(Bundle bundle, BundleEvent event) { log.log(LogService.LOG_INFO, String.format("adding bundle %s", bundle)); return lazyGetInfos(bundle); } private BundleInfos lazyGetInfos(Bundle bundle) { if(!bundleInfosMapping.containsKey(bundle)) { BundleInfos returned = new BundleInfos(); bundleInfosMapping.put(bundle, returned); } return bundleInfosMapping.get(bundle); } public void modifiedBundle(Bundle bundle, BundleEvent event, BundleInfos infos) { log.log(LogService.LOG_INFO, String.format("modifying bundle %s", bundle)); triggerReloadFor(infos); } public void triggerReloadFor(BundleInfos infos) { for(String controller : infos.getControllerClassNames()) { triggerReloadFor(controller); } } public void triggerReloadFor(String controllerClass) { if(displayedControllers.containsKey(controllerClass)) { log.log(LogService.LOG_INFO, String.format("Controller %s triggered client-side reload !", controllerClass)); publisher.send(BROWSER_WATCH_SOCKET, displayedControllers.get(controllerClass), "reload"); } } public void removedBundle(Bundle bundle, BundleEvent event, BundleInfos infos) { log.log(LogService.LOG_DEBUG, String.format("removing bundle %s", bundle)); // Just forget all infos, the list of displayed controllers is enough bundleInfosMapping.remove(bundle); } public void serviceChanged(ServiceEvent event) { Bundle bundle = event.getServiceReference().getBundle(); switch(event.getType()) { case ServiceEvent.REGISTERED: Object service = context.getService(event.getServiceReference()); if(service instanceof org.wisdom.api.Controller) { String serviceClassName = service.getClass().getName(); triggerReloadFor(serviceClassName); lazyGetInfos(bundle).addController(serviceClassName); log.log(LogService.LOG_INFO, String.format("Controller %s is registered !", serviceClassName)); } } } }