Reference Source

js/rendering/rendering_builder.js

// NPM IMPORTS
import assert from 'assert'
import _ from 'lodash'

// COMMON IMPORTS
import T                        from '../utils/types'
import {is_server}              from '../utils/is_browser'
import rendering_factory        from './rendering_factory'
import RenderingBuilderAssets   from './rendering_builder_assets'
import RenderingResolverBuilder from './rendering_resolver'


const context = 'common/rendering/rendering_builder'



/**
 * Rendering wrapper class.
 * @author Luc BORIES
 * @license Apache-2.0
 */
export default class RenderingBuilder extends RenderingBuilderAssets
{
    /**
     * Create a rendering wrapper class.
	 * 
	 * API:
	 * 		->constructor(arg_runtime, arg_assets_styles, arg_assets_scripts, arg_assets_img, arg_assets_html, arg_application)
	 * 
	 * 		->get_content_description(arg_view_name, arg_menubar_name):object - Get application content description.
	 * 		->get_initial_state(arg_view_name, arg_menubar_name):string - Get application initial state as a JSON string.
	 * 
	 * 		->render_html_page(arg_title, arg_view, arg_menubar, arg_credentials, arg_assets_services=undefined):string -  Render full page into HTML string.
	 * 		->render_page(arg_title, arg_view, arg_menubar, arg_credentials, arg_assets_services=undefined):RenderingResult - Render full page.
	 * 
	 * 		->render_json_content(arg_view, arg_menubar, arg_credentials, arg_assets_services):object - Render page content (inside 'content' DIV element) with a menubar and a view and convert rendering result to JSON.
	 * 		->render_content(arg_view, arg_menubar, arg_credentials, arg_assets_services):RenderingResult - Render page content (inside 'content' DIV element) with a menubar and a view.
	 * 
	 * @param {RuntimeBase} arg_runtime - runtime instance.
	 * @param {string} arg_assets_styles - application service name to provide style assets.
	 * @param {string} arg_assets_scripts - application service name to provide script assets.
	 * @param {string} arg_assets_img - application service name to provide image assets.
	 * @param {string} arg_assets_html - application service name to provide html assets.
	 * @param {TopologyDefineApplication} arg_application - application.
	 * 
	 * @returns {nothing}
     */
	constructor(arg_runtime, arg_assets_styles, arg_assets_scripts, arg_assets_img, arg_assets_html, arg_application)
	{
		assert( T.isObject(arg_runtime) && arg_runtime.is_base_runtime, context + ':constructor:bad runtime instance' )

		if ( T.isObject(arg_assets_styles) && T.isString(arg_assets_styles.style) && T.isString(arg_assets_styles.script) && T.isString(arg_assets_styles.image) && T.isString(arg_assets_styles.html) )
		{
			arg_application = arg_assets_scripts

			arg_assets_scripts = arg_assets_styles.script
			arg_assets_img = arg_assets_styles.image
			arg_assets_html = arg_assets_styles.html
			arg_assets_styles = arg_assets_styles.style
		}

		super(arg_runtime, arg_assets_styles, arg_assets_scripts, arg_assets_img, arg_assets_html, arg_application)
		
		assert( T.isObject(this._runtime) && this._runtime.is_base_runtime, context + ':constructor:bad this._runtime instance' )

		this.update_trace_enabled()
	}



	/**
	 * Get name for loggers.
	 * 
	 * @returns {string}
	 */
	get_name()
	{
		return 'RenderingBuilder instance'
	}
	
	
	
	/**
	 * Get application content description.
	 * 
	 * @param {string} arg_view_name - main view name.
	 * @param {string} arg_menubar_name - main menubar name.
	 * 
	 * @returns {object} - content description object as { name:..., type:..., state:..., settings:..., children:... }.
	 */
	get_content_description(arg_view_name, arg_menubar_name)
	{
		// console.log(context + ':get_content_description:view=%s, menubar=%s', arg_view_name, arg_menubar_name)
		return {
			name:'content',
			type:'page_content',
			state:{
				body_contents:[arg_menubar_name, 'separator', arg_view_name]
			},
			settings:{
				assets_urls_templates:{
					script:is_server() ? this.get_assets_script_url('{{url}}') : this._runtime.ui()._ui_rendering.get_asset_url('{{url}}', 'script', this._runtime.get_session_credentials()),
					style: is_server() ? this.get_assets_style_url('{{url}}')  : this._runtime.ui()._ui_rendering.get_asset_url('{{url}}', 'style',  this._runtime.get_session_credentials()),
					image: is_server() ? this.get_assets_image_url('{{url}}')  : this._runtime.ui()._ui_rendering.get_asset_url('{{url}}', 'image',  this._runtime.get_session_credentials()),
					html:  is_server() ? this.get_assets_html_url('{{url}}')   : this._runtime.ui()._ui_rendering.get_asset_url('{{url}}', 'html',   this._runtime.get_session_credentials())
				}
			},
			children:{
				separator:{
					name:'separator',
					type:'table',
					state:{},
					settings: {
						id:'separator',
						classes:'devapt-layout-persistent'
					}
				}
			}
		}
	}
	
	
	
	/**
	 * Get application initial state as a JSON string.
	 * 
	 * @param {string} arg_view_name - main view name.
	 * @param {string} arg_menubar_name - main menubar name.
	 * 
	 * @returns {string} - state JSON string.
	 */
	get_initial_state(arg_view_name, arg_menubar_name)
	{
		let initial_state = this._application ? this._application.export_settings() : {}

		initial_state.credentials = {
			"tenant":"{{{credentials_tenant}}}",
			"env":"{{{credentials_env}}}",
			"application":"{{{credentials_application}}}",
			
			"token":"{{{credentials_token}}}",
			"user_name":"{{{credentials_user_name}}}",
			"user_pass_digest":"{{{credentials_pass_digest}}}",

			"ts_login":"{{{credentials_login}}}",
			"ts_expiration":"{{{credentials_expire}}}",

			"errors_count":"{{{credentials_errors_count}}}",
			"renew_count":"{{{credentials_renew_count}}}"
		}

		initial_state.app_url    = this._application ? this._application.app_url : null
		initial_state.app_assets = this._application ? this._application.app_assets : null
		initial_state.commands   = this._application ? this._application.get_resources_settings('commands') : {}
		initial_state.views      = this._application ? this._application.get_resources_settings('views') : {}
		initial_state.menubars   = this._application ? this._application.get_resources_settings('menubars') : {}
		initial_state.menus      = this._application ? this._application.get_resources_settings('menus') : {}
		initial_state.models     = this._application ? this._application.get_resources_settings('models') : {}

		initial_state.assets_urls_templates = {
			script:this.get_assets_script_url('{{url}}'),
			style:this.get_assets_style_url('{{url}}'),
			image:this.get_assets_image_url('{{url}}'),
			html:this.get_assets_html_url('{{url}}')
		}

		initial_state.plugins_urls= {}
		if ( T.isArray(initial_state.used_plugins) )
		{
			const tenant = this._application.get_topology_owner()
			if (! tenant)
			{
				this.error('no owner tenant found for this application')
				console.error(context + ':get_initial_state:no owner tenant found for this application')
			} else {
				_.forEach(initial_state.used_plugins,
					(plugin_name)=>{
						// console.log('plugin_name', plugin_name)

						const plugin = tenant.get_topology_owner().plugin(plugin_name)
						if ( ! plugin )
						{
							console.error(context + ':get_initial_state:plugin not found for [' + plugin_name + ']')
							return
						}
						
						if ( T.isFunction(plugin.topology_plugin_instance.get_browser_plugin_file_url) )
						{
							const url = plugin.topology_plugin_instance.get_browser_plugin_file_url()
							if ( T.isString(url) )
							{
								initial_state.plugins_urls[plugin_name] = url
							}
						}
					}
				)
			}
		}
		
		if (! initial_state.views.content)
		{
			initial_state.views.content = this.get_content_description(arg_view_name, arg_menubar_name)
			// _.forEach(initial_state.views.content.children,
			// 	(view_desc, view_name)=>{
			// 		initial_state.views[view_name] = view_desc
			// 	}
			// )
		}

		return JSON.stringify(initial_state)
	}



	/**
	 * Render full page into HTML string.
	 * 
	 * @param {string|undefined} arg_title - page title, application title (optional).
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * @param {object} arg_assets_services - assets record (optional).
	 * 
	 * @returns {string} - rendering HTML code.
	 */
	render_html_page(arg_title, arg_view, arg_menubar, arg_credentials, arg_assets_services=undefined)
	{
		const rendering_result = this.render_page(arg_title, arg_view, arg_menubar, arg_credentials, arg_assets_services)
		const html = '<!DOCTYPE html>' + rendering_result.get_final_html('page')
		const rendered_html = this._runtime.context.render_credentials_template(html, arg_credentials)

		return rendered_html
	}



	/**
	 * Render full page.
	 * 
	 * @param {string|undefined} arg_title - page title, application title (optional).
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * @param {object} arg_assets_services - assets record (optional).
	 * 
	 * @returns {RenderingResult} - rendering result instance.
	 */
	render_page(arg_title, arg_view, arg_menubar, arg_credentials, arg_assets_services=undefined)
	{
		// DEBUG
		debugger
		console.log(context + ':render_page:credentials', arg_credentials)

		assert( T.isObject(arg_credentials) && arg_credentials.is_credentials, context + ':render_page:bad credentials object')
		
		// ONLY FOR SERVER SIDE
		assert( is_server(), context + ':render_page:only for server side')


		// SET ASSETS SERVICES NAMES
		if ( T.isObject(arg_assets_services) )
		{
			this.set_assets_services_names(arg_assets_services.style, arg_assets_services.script, arg_assets_services.image, arg_assets_services.html)
		}

		// GET TOPOLOGY DEFINED APPLICATION
		const topology_define_app = this.get_topology_defined_application(arg_credentials)
		assert(topology_define_app, context + ':render_page:bad topology_define_app')
		
		// GET VIEW
		const default_view_name = topology_define_app.app_default_view
		// const view = ( T.isString(arg_view) || ( T.isObject(arg_view) && arg_view.is_component ) ) ? arg_view : default_view_name
		
		// GET MENUBAR
		const default_menubar_name = topology_define_app.app_default_menubar
		// const menubar = ( T.isString(arg_menubar) || ( T.isObject(arg_menubar) && arg_menubar.is_component ) ) ? arg_menubar : default_menubar_name

		// GET DEFAULT TITLE IF NEEDED
		const default_title = topology_define_app.app_title
		const title = arg_title ? arg_title : default_title

		// TODO ADDSECURITY  HEADERS
		// const auth_basic_realm = '<meta http-equiv="WWW-Authenticate" content="Basic realm=Devtools"/>'
		// const auth_basic_credentials = '<meta http-equiv="Authorization" content="Basic {{{credentials_basic_base64}}}"/>'

		const view_name = T.isString(arg_view) ? arg_view : ( ( T.isObject(arg_view) && arg_view.is_component ) ? arg_view.get_name() : default_view_name )
		const menubar_name = T.isString(arg_menubar) ? arg_menubar : ( ( T.isObject(arg_menubar) && arg_menubar.is_component ) ? arg_menubar.get_name() : default_menubar_name )
		const content_result = this.render_json_content(view_name, menubar_name, arg_credentials, arg_assets_services)
		const stored_state = this.get_initial_state(view_name, menubar_name)

		const page = {
			type:'page',
			state:{
				title:title,
				metas:undefined,

				body_headers:undefined,
				body_contents:[],
				body_footers:undefined,
				
				head_styles_tags:content_result.head_styles_tags,
				head_styles_urls:topology_define_app.app_assets_css.concat(content_result.head_styles_urls),
				
				head_scripts_tags:content_result.head_scripts_tags,
				head_scripts_urls:content_result.head_scripts_urls,

				body_styles_tags:content_result.body_styles_tags,
				body_styles_urls:content_result.body_styles_urls,

				body_scripts_tags:content_result.body_scripts_tags, // SEE LATER FOR CONTENT
				body_scripts_urls:[
					{
						id:'js-socketio',
						src:'/socket.io/socket.io.js',
						absolute:true
					}
				].concat(content_result.body_scripts_urls, topology_define_app.app_assets_js)
			},
			settings:{
				html_lang:undefined,
				html_class:undefined,
				html_prefix:undefined,

				head_charset:'utf-8',
				head_viewport:undefined,
				head_description:undefined,
				head_robots:undefined,

				body_class:undefined,

				assets_urls_templates:{
					script:this.get_assets_script_url('{{url}}'),
					style:this.get_assets_style_url('{{url}}'),
					image:this.get_assets_image_url('{{url}}'),
					html:this.get_assets_html_url('{{url}}')
				}
			},
			children:{}
		}

		const rendering_context = {
			trace_fn:undefined, /*console.log,*/
			resolver:RenderingResolverBuilder.from_topology('server resolver from topology for page', topology_define_app),
			credentials:arg_credentials,
			rendering_factory:rendering_factory
		}
		
		content_result.head_styles_urls = []
		content_result.head_styles_tagss = []
		content_result.head_scripts_urls = []
		content_result.head_scripts_tags = []
		content_result.body_styles_urls = []
		content_result.body_styles_tags = []
		content_result.body_scripts_urls = []
		content_result.body_scripts_tags = []
		
		content_result.assets_urls_templates = page.settings.assets_urls_templates

		// console.log(context + ':render_page:content_result', content_result.vtrees.content.c)

		let content_json_str_result = JSON.stringify(content_result)
		content_json_str_result = content_json_str_result.replace(/"class"/g, '"className"')

		page.state.body_scripts_tags = page.state.body_scripts_tags.concat([
			{
				id:'js-initial-state',
				content:`window.__INITIAL_STATE__ = ${stored_state};`
			},
			{
				id:'js-initial-content',
				content:`window.__INITIAL_CONTENT__ = ${content_json_str_result};`
			}
		])

		const rendering_result = rendering_factory(page, rendering_context, page.children)
		return rendering_result
	}

	

	/**
	 * Render page content (inside 'content' DIV element) with a menubar and a view and convert rendering result to JSON.
	 * 
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * @param {object} arg_assets_services - assets record (optional).
	 * 
	 * @returns {object} - rendering result converted to JSON object.
	 */
	render_json_content(arg_view, arg_menubar, arg_credentials, arg_assets_services)
	{
		const result = this.render_content(arg_view, arg_menubar, arg_credentials, arg_assets_services)
		const json = result.convert_to_json()
		return json
	}

	

	/**
	 * Render page content (inside 'content' DIV element) with a menubar and a view.
	 * 
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * @param {object} arg_assets_services - assets record (optional).
	 * 
	 * @returns {RenderingResult} - rendering result instance.
	 */
	render_content(arg_view, arg_menubar, arg_credentials, arg_assets_services)
	{
		assert( T.isString(arg_view) || ( T.isObject(arg_view) && arg_view.is_component ), context + ':bad view string or object')
		assert( T.isObject(arg_credentials) && arg_credentials.is_credentials, context + ':bad credentials object')
		
		// SET ASSETS SERVICES NAMES
		if ( T.isObject(arg_assets_services) )
		{
			this.set_assets_services_names(arg_assets_services.style, arg_assets_services.script, arg_assets_services.image, arg_assets_services.html)
		}

		// RUN ON SERVER SIDE
		if ( is_server() )
		{
			assert( T.isString(arg_view) || T.isString(arg_menubar), context + ':bad view or menubar name string')
			
			// RUN ON BROWSER SIDE
			const rendering_result = this._render_content_on_server(arg_view, arg_menubar, arg_credentials)
			return rendering_result
		}

		// RUN ON BROWSER SIDE
		const rendering_result = this._render_content_on_browser(arg_view, arg_menubar, arg_credentials)
		return rendering_result
	}


	/**
	 * Get rendering function resolver.
	 * 
	 * @param {Credentials} arg_credentials - credentials instance.
	 * 
	 * @returns {Function} - rendering function resolver.
	 */
	static get_rendering_function_resolver(arg_credentials)
	{
		if ( is_server() )
		{
			// GET TOPOLOGY DEFINED APPLICATION
			const topology_define_app = this.get_topology_defined_application(arg_credentials)
			assert(topology_define_app, context + ':render_content:bad topology_define_app')
			
			// GET SERVER RESOLVER
			const resolver = RenderingResolverBuilder.from_topology('server resolver from topology', topology_define_app)
			return resolver
		}

		// GET BROWSER RESOLVER
		const res_resolver = window.devapt().ui().get_resource_description_resolver()
		const rf_resolver  = window.devapt().ui().get_rendering_function_resolver()
		const rendering_resolver = RenderingResolverBuilder.from_resolvers('browser resolver from ui', res_resolver, rf_resolver)
		return rendering_resolver
	}



	/**
	 * Render page content on browser side.
	 * @private
	 * 
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * 
	 * @returns {RenderingResult} - rendering result instance.
	 */
	_render_content_on_browser(arg_view, arg_menubar, arg_credentials)
	{
		assert( T.isObject(this._runtime) && this._runtime.is_base_runtime, context + ':_render_content_on_browser:bad this._runtime instance' )
		
		const store = this._runtime.get_state_store()
		const state = store.get_state()

		const res_resolver = this._runtime.ui().get_resource_description_resolver()
		const rf_resolver  = this._runtime.ui().get_rendering_function_resolver()
		const rendering_resolver = RenderingResolverBuilder.from_resolvers('browser resolver from ui', res_resolver, rf_resolver)

		// GET DEFAULT VIEW
		const default_view_name = state.get('default_view', undefined)
		
		// GET DEFAULT MENUBAR
		const default_menubar_name = state.get('default_menubar', undefined)
		
		return this._render_content_common(arg_view, arg_menubar, arg_credentials, default_view_name, default_menubar_name, rendering_resolver)
	}



	/**
	 * Render page content on server side.
	 * @private
	 * 
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * 
	 * @returns {RenderingResult} - rendering result instance.
	 */
	_render_content_on_server(arg_view, arg_menubar, arg_credentials)
	{
		// GET TOPOLOGY DEFINED APPLICATION
		const topology_define_app = this.get_topology_defined_application(arg_credentials)
		assert(topology_define_app, context + ':render_content:bad topology_define_app')

		// GET DEFAULT VIEW
		const default_view_name = topology_define_app.app_default_view
		
		// GET DEFAULT MENUBAR
		const default_menubar_name = topology_define_app.app_default_menubar
		
		const resolver = RenderingResolverBuilder.from_topology('server resolver from topology', topology_define_app)

		return this._render_content_common(arg_view, arg_menubar, arg_credentials, default_view_name, default_menubar_name, resolver)
	}



	/**
	 * Render page content helper on browser or server side.
	 * @private
	 * 
	 * @param {string|Component|undefined} arg_view - main view name or instance (optional) (default application view name).
	 * @param {string|Component|undefined} arg_menubar - main menubar name or instance (optional) (default application menubar name).
	 * @param {Credentials} arg_credentials - credentials instance.
	 * @param {string} arg_default_view_name - default view name.
	 * @param {string} arg_default_menubar_name - default menubar name.
	 * @param {RenderingResolver} arg_rendering_resolver - object with 'find_rendering_function(type name)' method.
	 * 
	 * @returns {RenderingResult} - rendering result instance.
	 */
	_render_content_common(arg_view, arg_menubar, arg_credentials, arg_default_view_name, arg_default_menubar_name, arg_rendering_resolver)
	{
		const menubar = (arg_menubar ? arg_menubar : arg_default_menubar_name)
		const view    = (arg_view    ? arg_view    : arg_default_view_name)
		const content = this.get_content_description(view, menubar)

		// DEBUG
		// console.log(context + ':_render_content_common:arg_view_name=%s,arg_menubar_name=%s,view=%s,menubar=%s', arg_view_name, arg_menubar_name, view, menubar)
		// console.log(context + ':_render_content_common:content', content)

		const rendering_context = {
			trace_fn:undefined,//console.log,//
			resolver:arg_rendering_resolver,
			credentials:arg_credentials,
			rendering_factory:rendering_factory
		}
		
		const rendering_result = rendering_factory(content, rendering_context, undefined)
		
		rendering_result.assets_urls_templates = content.settings.assets_urls_templates

		return rendering_result
	}
}