All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cogchar.lifter.model.main.PageCommander.scala Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2012 by The Cogchar Project (www.cogchar.org).
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.cogchar.lifter.model.main
import org.cogchar.name.lifter.{ActionStrings}

import net.liftweb.http.ListenerManager
import scala.xml.NodeSeq
import net.liftweb.actor.LiftActor
import org.appdapter.core.name.{FreeIdent, Ident}
import org.cogchar.impl.web.util.HasLogger
import org.cogchar.impl.web.wire.{LifterState, SessionOrganizer, WebappGlobalState, WebSessionState, WebappCommander}
import org.cogchar.lifter.model.handler.{HandlerConfigurator}
import org.cogchar.lifter.model.action.{AbstractLifterActionHandler, LifterVariableHandler}
import org.cogchar.lifter.model.control.{AbstractControlSnippet}
import org.cogchar.lifter.view.TextBox
import org.cogchar.api.web.{WebControl}
import org.cogchar.impl.web.config.{WebControlImpl, LiftAmbassador, LiftConfig, WebInstanceGlob}
import scala.collection.JavaConverters._
// import org.cogchar.platform.trigger.CogcharActionBinding
	
/**
 *
 * @author Ryan Biggs 
 */
	
// We might eventually want parts of PageCommander to be performed via a class of 
// actors with an instance of that class created for each session.

object PageCommander extends LiftActor with ListenerManager with HasLogger with WebappCommander {
      
	def info(msg: String, params: Any*) {
		myLogger.info(msg, params.map(_.asInstanceOf[Object]).toArray)
	}
  
	private lazy val myAmbassador:LiftAmbassador = LiftAmbassador.getLiftAmbassador
	def getLiftAmbassador = myAmbassador
	  
	private var updateInfo: String = ""
	  
	def createUpdate = updateInfo

	private lazy val myGlobalStateMgr = new WebappGlobalState
		
	override def  getInitConfig() : LiftConfig = getLiftAmbassador.getInitialConfig
	override def getInitialConfigId : String = getSessionOrg.getInitialConfigID

	def exposedUpdateListeners(x : Any) : Unit = {updateListeners(x)}

	private lazy val myMessenger: CogcharMessenger =  new CogcharMessenger(this, myGlobalStateMgr)
	def getMessenger: WebInstanceGlob = myMessenger
	def getSessionOrg : SessionOrganizer = myMessenger
	
	
	private def getSessionState(sessionId:String) = getSessionOrg.getSessionState(sessionId)
	def hackIntoSnippetDataMap(sessionId:String) =   getSessionOrg.hackIntoSnippetDataMap(sessionId)

	private val myHeadActHandler : AbstractLifterActionHandler = HandlerConfigurator.initializeActionHandlers(getLiftAmbassador, myGlobalStateMgr, getSessionOrg, this)
	private val myHeadControlSnippet : AbstractControlSnippet = HandlerConfigurator.initializeControlSnippets(getSessionOrg)
	  

	
	
	def getMarkup(sessionId:String, controlId: Int): NodeSeq = {
		var nodeOut = NodeSeq.Empty
		try {
			nodeOut = getSessionState(sessionId).controlXmlBySlot(controlId)
		} catch {
			case _: Any => // Implies nothing in map for this controlId, do nothing and return empty nodeOut
		}
		//info("nodeOut for session " + sessionId + " and control " + controlId + " is " + nodeOut) // TEST ONLY
		nodeOut
	}
	  
	
	def renderInitialControls {
		// myWebappState.lifterInitialized = true
		val so = getSessionOrg
		so.markSessionGroupInitFlag(true)
		so.renderInitControlsOnAllSessions
		/*
		// If this is a restart on config change, activeSessions will have sessions which need to be re-initialized
		myWebappState.activeSessions.foreach(sessionId => initializeSessionAndRedirectToNewTemplate(sessionId))
		// If it's an initial startup, there may be sessions in sessionsAwaitingStart
		myWebappState.sessionsAwaitingStart.foreach(sessionId => initializeSessionAndRedirectToNewTemplate(sessionId))
		myWebappState.sessionsAwaitingStart.clear
		 */
	}
	  
	override def initializeSessionAndRedirectToNewTemplate(sessionId:String) {
		getSessionOrg.initializeSession(sessionId);
		val currentTemplateName : String = getSessionOrg.getCurrentTemplateForSession(sessionId)
		// updateListeners(HtmlPageRequest(sessionId, Some(currentTemplateName))
		sendSessionPage(sessionId, Some(currentTemplateName))
		
		setControlsFromMap(sessionId)
	}
	  
	  
	// This method is used by templates to check if session is active on initial render. If not, session should
	// be initialized and the caller notified it wasn't previously active by returning a false result
	def checkForActiveSessionAndStartIfNot(sessionId:String):Boolean = {
		val so = getSessionOrg
		var sessionActive = true
		if (!(so.hasActiveSession(sessionId))) {
			sessionActive = false
			so.requestStart(sessionId) // Needs a little more refactoring between this and requestStart
			if (so.isSessionGroupStarted()) {
				setControlsFromMap(sessionId)
			}
		} 
		return sessionActive
	}
	override def initFromCogcharRDF(sessionId:String, liftConfig:LiftConfig) {
		info("Loading LiftConfig for session {}", sessionId)

		val so = getSessionOrg
		so.initSession(sessionId)
		val sessionState : WebSessionState = getSessionState(sessionId)
		sessionState.currentLiftConfig = liftConfig

		val sillyMaxControlCount = myGlobalStateMgr.getMaxControlCount()
		
		val controlList: java.util.List[WebControlImpl] = liftConfig.myCCs

		val controlSet = controlList.asScala.toSet
		controlSet.foreach(controlDef => {
				var slotNum:Int = -1
				try {
					val finalSplitterIndex = controlDef.myURI_Fragment.lastIndexOf("_")
					slotNum = controlDef.myURI_Fragment.splitAt(finalSplitterIndex+1)._2.toInt
				} catch {
					case _: Any =>  myLogger.warn("Unable to get valid slotNum from loaded control; URI fragment was {}", controlDef.myURI_Fragment) // The control will still be loaded into slot -1; could "break" here but it's messy and unnecessary
				}
			
				if (slotNum > sillyMaxControlCount) {
					myLogger.warn("Maximum number of controls exceeded ({}); some controls may not be cleared upon page change!", sillyMaxControlCount)
					myLogger.warn("MAX_CONTROL_QUANTITY in LifterState can be increased if this is necessary.")
				}
				loadControlDefToState(sessionId, slotNum, controlDef)
			})
		// Blank unspecified slots (out to MAX_CONTROL_QUANTITY)
		for (slot <- 1 to sillyMaxControlCount) {
			sessionState.controlXmlBySlot.putIfAbsent(slot, NodeSeq.Empty)
		}
		sessionState.currentTemplateName = liftConfig.template
		val curTemplName = sessionState.currentTemplateName
		if (sessionId.equals(getInitialConfigId)) { 
			renderInitialControls; // Required to get things started if pages are loaded in browsers before config is initialized
		} else { // otherwise...
			
			val changedTemplate = !curTemplName.equals(sessionState.lastLiftConfig.template)
			// ... load new template if necessary
			if (changedTemplate) {
	
				sendSessionPage(sessionId, Some(curTemplName))
			} 
			// ... load new controls
			setControlsFromMap(sessionId)
		}
	}
	override def getXmlForControl(sessionId: String, slotNum:Int, controlDef:WebControl): NodeSeq = {
		myHeadControlSnippet.generateOutputXml(sessionId, slotNum, controlDef)
	}
	
	def setControl(sessionId: String, slotNum: Int, slotHtml: NodeSeq) {
		getSessionState(sessionId).controlXmlBySlot(slotNum) = slotHtml 
		sendSessionControlChange(sessionId, slotNum, slotHtml)
	}
	def sendSessionPage(sessionId : String, templateName_opt : Option[String]) {
		updateListeners(HtmlPageRequest(sessionId, templateName_opt))
	}

	def sendSessionControlChange(sessionId : String, slotNum : Int, nodeSeq : NodeSeq) {
		updateListeners(ControlChange(sessionId, slotNum, nodeSeq))
	}	  
	def setControlsFromMap(sessionId:String) {
		val sstate = getSessionState(sessionId)
		val cxbs = sstate.controlXmlBySlot
		val slotIterator = cxbs.keysIterator
		while (slotIterator.hasNext) {
			val nextSlot : Int = slotIterator.next
			val nodeSeq : NodeSeq = cxbs(nextSlot)
			sendSessionControlChange(sessionId, nextSlot, nodeSeq)
		}
	}							

	override def loadControlDefToState(sessionId:String, slotNum:Int, controlDef:WebControl) {
		val sessionState = getSessionState(sessionId)
		// Below, we clone the controlDef with a copy constructor so the ControlConfigs in the controlDefMap are 
		// not the same objects as in LiftAmbassador's page cache.
		// That's important largly because ToggleButton modifies the actions in the controlDefMap.
		// Pretty darn messy, and likely a topic for further refactoring.
		val clonedWCI =  new WebControlImpl(controlDef)
		sessionState.controlConfigBySlot(slotNum) = clonedWCI
		// Trigger control initialization handler chain to fill proper XML into controlsMap
		sessionState.controlXmlBySlot(slotNum) = getXmlForControl(sessionId, slotNum, controlDef)
		// Check for initial nee "local" actions which PageCommander needs to handle, such as text display
		myHeadActHandler.optionalInitialRendering(sessionId, slotNum, clonedWCI) //  controlDef)
	}
  
	def handleAction(sessionId:String, formId:Int, input:Array[String]) {
		//info("Handling action: {}", getSessionState(sessionId).controlConfigBySlot(formId).action) // TEST ONLY
		myHeadActHandler.processAction(sessionId, formId, 
									   getSessionState(sessionId).controlConfigBySlot(formId), input)
	}
	  
	import java.util.Date // needed for currently implemented "debouncing" function
	// Maps controls with actions only (buttons) to action handlers
	def triggerAction(sessionId:String, id: Int) {
		// Check last actuated time for this control, and ignore if it happened less than IGNORE_BOUNCE_TIME ago
		val ignore = checkForBounce(sessionId, id)
		if (!ignore) {
			val sState = getSessionState(sessionId)
			if (sState.toggleControlStateBySlot contains id) {
				ControlToggler.getTheToggler.toggle(sState,id)
			}
			handleAction(sessionId, id, null)		  
		}
	}
	  
	val bounceCheckLock: Object = new Object()
	final val IGNORE_BOUNCE_TIME = 250 //ms
	def checkForBounce(sessionId:String, id:Int): Boolean = {
		bounceCheckLock.synchronized {
			val time = new Date().getTime()
			val bounceMapMap = getSessionOrg.getLastTimeAcutatedBySlot
			val sessionBounceMap = bounceMapMap(sessionId)
			var ignore = false
			//info("Checking for bounce with session " + sessionId + " and slot " + id + " time=" + time) // TEST ONLY
			if (sessionBounceMap contains id) {
				//info("Last time=" + bounceMap(id) + " diff=" + (time - bounceMap(id))) // TEST ONLY
				if ((time - sessionBounceMap(id)) < IGNORE_BOUNCE_TIME) {
					ignore = true;
					myLogger.warn("Debouncing control {} in session {}", id, sessionId)
				}
			}
			sessionBounceMap(id) = time
			ignore
		}
	}
	  
	// Handles incoming messages from controls
	override def lowPriority : PartialFunction[Any, Unit]  = {		
		case a:ControlAction => {
				triggerAction(a.sessionId, a.slotNum)
			}
		case a:ControlTextInput => {
				handleAction(a.sessionId:String, a.slotNum, a.text)
			}
			// MultiSelectControls send substrings of the action string to the action handlers (as if they were text input) 
			// according to the selected item
		case a:ControlMultiSelect => {
				handleMultiSelectInput(a.sessionId, a.slotNum, a.subControl)
			}
			// MultiActionControls execute actions, populated in the LifterState.SessionState.multiActionsBySlot by "initial actions"
			// upon control rendering, according to the selected item
		case a:ControlMultiAction => {
				val state = getSessionOrg.getSessionState(a.sessionId)
				if (!a.multiActionFlag) {
					// If the multiActionFlag is not set, handle this control as a MultiSelect control.
					// Allows both MultiSelect and MultiAction controls to extend AbstractMultiSelectControl
					handleMultiSelectInput(a.sessionId, a.slotNum, a.subControl)
				} else if (state.multiActionsBySlot contains a.slotNum) {
					// Set the "main" action to the currently desired one; a workaround we may want to change to something more elegant in the future
					state.controlConfigBySlot(a.slotNum).action = 
						state.multiActionsBySlot(a.slotNum)(a.subControl)
					// Using triggerAction to check for bounce, which I believe should work properly here...
					triggerAction(a.sessionId, a.slotNum)
				} else {
					myLogger.warn("Multi action control is attempting to execute an action, but no multiAction data found in Lifter state. " +
								  "Session = {}; slot = {}", a.sessionId, a.slotNum)
				}
			}
		case _: Any =>
	}
	  
	def handleMultiSelectInput(sessionId:String, slotNum:Int, subControlSelected:Int) {
		val input:Array[String] = Array(ActionStrings.subControlIdentifier + subControlSelected.toString)
		handleAction(sessionId, slotNum, input)
	}

	  

	  
	
	
}

  





© 2015 - 2025 Weber Informatics LLC | Privacy Policy