module Fiona7 module Builder class ObjBuilder end end end require 'fiona7/type_register' require 'fiona7/obj_class_name_mangler' require 'fiona7/obj_class_name_demangler' #require 'fiona7/write_obj' #require 'fiona7/released_obj' require 'fiona7/builder/batch_widget_writer' require 'fiona7/widget_resolver' require 'fiona7/prefetch/obj_prefetch' require 'fiona7/attribute_writers/factory' require 'fiona7/widget_gc/garbage_collector' module Fiona7 module Builder class ParamPreprocessor def self.call(*args) self.new.call(*args) end def call(params) remove_rack_formdata_workaround(params.symbolize_keys) end protected # Rack is unable to handle the data format mandated by scrivito: # obj[_widget_pool][somewidget][widget_attr][][widgetlist] # obj[_widget_pool][somewidget][widget_attr][][]=otherwidget1 # obj[_widget_pool][somewidget][widget_attr][][]=otherwidget2 # # thus we introduce an extended format to work around this # problem and still be able to send API requests through # form-data: # # obj[_widget_pool][somewidget][widget_attr][][widgetlist] # obj[_widget_pool][somewidget][widget_attr][formdata$workaround][]=otherwidget1 # obj[_widget_pool][somewidget][widget_attr][formdata$workaround][]=otherwidget2 # # This method normalizes the extended format to the usual one def remove_rack_formdata_workaround(params) if params.kind_of?(Hash) if params.has_key?("formdata$workaround") params["formdata$workaround"] else Hash[ params.map { |key, value| [key, remove_rack_formdata_workaround(value)] } ] end elsif params.kind_of?(Array) params.map { |value| remove_rack_formdata_workaround(value) } else params end end end class ObjBuilder def initialize(values) @values = ParamPreprocessor.call(values) # garbage @values.delete(:_modification) # revert command sends this info. which is silly. @values.delete(:_last_changed) end def build assert_valid prepare_object write_widget_pool store_attributes widgets_gc @obj end def validate true end protected def update? false end def assert_valid validate || (raise Scrivito::ClientError.new(@error_msg || "Invalid input", 422)) end def prepare_object @path = @values.delete(:_path) || generate_orphaned_path @obj_class = @values.delete(:_obj_class) @real_obj_class = Fiona7::ObjClassNameMangler.new(@obj_class).mangle @widget_pool = @values.delete(:_widget_pool) @permalink = @values.key?(:_permalink) ? (@values.delete(:_permalink) || "") : nil @path = "/#{@path}" unless @path.start_with?('/') @name, parent_path = name_and_parent_path_from_path(@path) @parent = ensure_parent_exists(parent_path) if parent_path ensure_obj_class_exists @obj = WriteObj.create!(name: @name, parent_obj_id: @parent.id, obj_class: @real_obj_class) end def write_widget_pool resolver = WidgetResolver.new(@obj.attr_values["X_widget_pool"]||[], Prefetch::ObjPrefetch.new(WriteObj)) @id_map = resolver.id_map @widget_path_map = resolver.path_map if @widget_pool && !@widget_pool.empty? @new_full_text = rewrite_full_text @widgets = BatchWidgetWriter.new(@obj, @id_map, @widget_pool, @widget_path_map) @widgets.write # FIXME: refactor this code if @widgets.pool_changed? # after widget writing this either has new widgets # or some widgets have been removed @new_widget_pool = @widget_pool.map do |widget_id, definition| # widget deleted next if definition.nil? {title: widget_id, destination_object: @widget_path_map[widget_id]} end.compact # newly added widgets @widget_path_map.each do |widget_id, path| next if !@widget_pool[widget_id].nil? @new_widget_pool << {title: widget_id, destination_object: path} end end end end def store_attributes if @obj.obj_class != @real_obj_class @obj.obj_class = @real_obj_class @obj.save! @obj.reload end @obj.send(:reload_attributes) factory = Fiona7::AttributeWriters::Factory.new(@obj, @obj_class, WriteObj, @widget_path_map) @values.each do |attribute_name, possible_pair| (claimed_type, value) = *possible_pair worker = factory.call(attribute_name) worker.call(value, claimed_type) end if (@new_full_text) @obj.set(:X_full_text, @new_full_text) end @obj.set(:X_widget_pool, @new_widget_pool) if @new_widget_pool @obj.set(:permalink, @permalink) if @permalink @obj.save! @obj.edit! unless @obj.really_edited? end def widgets_gc if self.trigger_widget_gc? WidgetGc::GarbageCollector.new(@obj.id, :update).gc! end end def trigger_widget_gc? # NOTE: there is actually only one case when we need # to trigger widget gc: when a widget has been deleted # # Sadly deleting widgets has been deprecated from the # SDK - now only the references to a widget are # !implicitely! removed. # # It could be possible to guess which widget was deleted # but its tricky and complicates things. # # Therefore we use a simple heuristic: if a widgetlist # attribute has been provided as an input, then # widgets have been added/rearanged/deleted which # triggers widget garbage collection. # # The overhead of useless widget garbage collection pass # is equal to two database queries, so it isn't too bad. params_contain_widgetlist = @values.present? && @values.any? {|name, pair| pair.is_a?(Array) && pair[0] == "widgetlist" } pool_contains_widgetlist = @widget_pool.present? && @widget_pool.any? {|widget_id, values| values.nil? || values.any? {|name, pair| pair.is_a?(Array) && pair[0] == "widgetlist" } } params_contain_widgetlist || pool_contains_widgetlist end def rewrite_full_text full_text = ::YAML.load(@obj.attr_values["X_full_text"]) rescue {} full_text = {} unless full_text.kind_of?(Hash) full_text["_widget_pool"] ||= {} full_text["_widget_pool"].deep_merge!(@widget_pool) full_text.to_yaml rescue => e Rails.logger.error("Unable to store information for search engine: #{e.message}") nil end def name_and_parent_path_from_path(path) components = path.split('/') name = components.pop.presence parent_path= components.join('/').presence || '/' return name, parent_path end def ensure_parent_exists(path) remaining = path.split("/") current = [] paths = [] original = path while !remaining.empty? current.push(remaining.shift) path = current.join('/').presence || '/' paths.push(path) end paths.each do |path| if !WriteObj.exists?(path: path) name, parent_path = name_and_parent_path_from_path(path) WriteObj.create!(name: name, parent_obj_id: WriteObj.find_by_path(parent_path).id, obj_class: 'X_Container') end end WriteObj.find_by_path(original) || (raise "Tried to make sure that the parent under '#{original}' exist, but it does not :(") end def generate_orphaned_path return nil if update? "_orphaned/#{SecureRandom.hex(16)}" end def ensure_obj_class_exists values = @values.with_indifferent_access obj_class = @obj_class Fiona7::TypeRegister.instance.ad_hoc_synchronize( Fiona7::TypeRegister::AdHocTypeDefinition.new(values, obj_class).type_definition ) end end end end