From 40688ec5df62188afdfbfaa92bce95e69a64c0ae Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Fri, 9 Apr 2010 19:02:39 +0000 Subject: [PATCH] # Initial support for automatic validation of entity template XML. Add RelaxNG schemas for all current components. Add -dumpSchema command-line option to dump the combined entity schema. Add a Perl script to validate entity templates against the schema. See #413. This was SVN commit r7452. --- .../public/simulation/components/Armour.js | 26 ++- .../public/simulation/components/Attack.js | 34 ++++ .../public/simulation/components/Builder.js | 9 + .../mods/public/simulation/components/Cost.js | 33 +++- .../public/simulation/components/Health.js | 20 +++ .../public/simulation/components/Identity.js | 19 +++ .../simulation/components/ResourceGatherer.js | 18 ++ .../simulation/components/ResourceSupply.js | 30 ++++ .../public/simulation/components/Sound.js | 10 ++ source/ps/GameSetup/GameSetup.cpp | 15 +- source/simulation2/Simulation2.cpp | 5 + source/simulation2/Simulation2.h | 2 + .../simulation2/components/CCmpFootprint.cpp | 17 ++ .../components/CCmpObstruction.cpp | 7 + .../simulation2/components/CCmpPosition.cpp | 39 ++--- .../simulation2/components/CCmpUnitMotion.cpp | 7 + .../components/CCmpVisualActor.cpp | 13 +- source/simulation2/system/Component.h | 4 +- .../simulation2/system/ComponentManager.cpp | 85 +++++++++- source/simulation2/system/ComponentManager.h | 7 +- source/simulation2/system/IComponent.cpp | 6 + source/simulation2/system/IComponent.h | 2 + source/tools/entvalidate/entvalidate.pl | 154 ++++++++++++++++++ 23 files changed, 518 insertions(+), 44 deletions(-) create mode 100644 source/tools/entvalidate/entvalidate.pl diff --git a/binaries/data/mods/public/simulation/components/Armour.js b/binaries/data/mods/public/simulation/components/Armour.js index 904e3c3d23..f64b49d8b9 100644 --- a/binaries/data/mods/public/simulation/components/Armour.js +++ b/binaries/data/mods/public/simulation/components/Armour.js @@ -1,5 +1,16 @@ function Armour() {} +Armour.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Armour.prototype.Init = function() { }; @@ -7,10 +18,9 @@ Armour.prototype.Init = function() Armour.prototype.TakeDamage = function(hack, pierce, crush) { // Adjust damage values based on armour - // (Default armour values to 0 if undefined) - var adjHack = Math.max(0, hack - (this.template.Hack || 0)); - var adjPierce = Math.max(0, pierce - (this.template.Pierce || 0)); - var adjCrush = Math.max(0, crush - (this.template.Crush || 0)); + var adjHack = Math.max(0, hack - this.template.Hack); + var adjPierce = Math.max(0, pierce - this.template.Pierce); + var adjCrush = Math.max(0, crush - this.template.Crush); // Total is sum of individual damages, with minimum damage 1 var total = Math.max(1, adjHack + adjPierce + adjCrush); @@ -22,11 +32,11 @@ Armour.prototype.TakeDamage = function(hack, pierce, crush) Armour.prototype.GetArmourStrengths = function() { - // Convert attack values to numbers, default 0 if unspecified + // Convert attack values to numbers return { - hack: +(this.template.Hack || 0), - pierce: +(this.template.Pierce || 0), - crush: +(this.template.Crush || 0) + hack: +this.template.Hack, + pierce: +this.template.Pierce, + crush: +this.template.Crush }; }; diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 9e51cae7df..98f993ca36 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -1,5 +1,39 @@ function Attack() {} +Attack.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Attack.prototype.Init = function() { }; diff --git a/binaries/data/mods/public/simulation/components/Builder.js b/binaries/data/mods/public/simulation/components/Builder.js index 9fb6b1b831..9bd114fcee 100644 --- a/binaries/data/mods/public/simulation/components/Builder.js +++ b/binaries/data/mods/public/simulation/components/Builder.js @@ -1,5 +1,14 @@ function Builder() {} +Builder.prototype.Schema = + "" + + "tokens" + + "" + + "" + + "" + + "" + + ""; + Builder.prototype.Init = function() { }; diff --git a/binaries/data/mods/public/simulation/components/Cost.js b/binaries/data/mods/public/simulation/components/Cost.js index 5d1aabf62f..93cc7f6bd7 100644 --- a/binaries/data/mods/public/simulation/components/Cost.js +++ b/binaries/data/mods/public/simulation/components/Cost.js @@ -1,5 +1,30 @@ function Cost() {} +Cost.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Cost.prototype.Init = function() { }; @@ -26,10 +51,10 @@ Cost.prototype.GetBuildTime = function() Cost.prototype.GetResourceCosts = function() { return { - "food": +(this.template.Resources.food || 0), - "wood": +(this.template.Resources.wood || 0), - "stone": +(this.template.Resources.stone || 0), - "metal": +(this.template.Resources.metal || 0) + "food": +this.template.Resources.food, + "wood": +this.template.Resources.wood, + "stone": +this.template.Resources.stone, + "metal": +this.template.Resources.metal }; }; diff --git a/binaries/data/mods/public/simulation/components/Health.js b/binaries/data/mods/public/simulation/components/Health.js index 9b739521e7..14e2a4497e 100644 --- a/binaries/data/mods/public/simulation/components/Health.js +++ b/binaries/data/mods/public/simulation/components/Health.js @@ -1,5 +1,25 @@ function Health() {} +Health.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "corpse" + + "" + + ""; + Health.prototype.Init = function() { // Default to , but use if it's undefined or zero diff --git a/binaries/data/mods/public/simulation/components/Identity.js b/binaries/data/mods/public/simulation/components/Identity.js index 9127bb7c3c..f448a123eb 100644 --- a/binaries/data/mods/public/simulation/components/Identity.js +++ b/binaries/data/mods/public/simulation/components/Identity.js @@ -1,5 +1,24 @@ function Identity() {} +Identity.prototype.Schema = + "" + + "" + // TODO: ? + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Identity.prototype.Init = function() { }; diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js index 6ec33a945b..3af00175f8 100644 --- a/binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -1,5 +1,23 @@ function ResourceGatherer() {} +ResourceGatherer.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + ResourceGatherer.prototype.Init = function() { }; diff --git a/binaries/data/mods/public/simulation/components/ResourceSupply.js b/binaries/data/mods/public/simulation/components/ResourceSupply.js index 4fdcb97fae..21189c81f6 100644 --- a/binaries/data/mods/public/simulation/components/ResourceSupply.js +++ b/binaries/data/mods/public/simulation/components/ResourceSupply.js @@ -1,5 +1,35 @@ function ResourceSupply() {} +ResourceSupply.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "food" + + "fish" + + "" + + "" + + "food" + + "fruit" + + "" + + "" + + "food" + + "grain" + + "" + + "" + + "food" + + "meat" + + "" + + "" + + "food" + + "milk" + + "" + + "wood" + + "stone" + + "metal" + + ""; + ResourceSupply.prototype.Init = function() { // Current resource amount (non-negative; can be a fractional amount) diff --git a/binaries/data/mods/public/simulation/components/Sound.js b/binaries/data/mods/public/simulation/components/Sound.js index d3e8215daa..2518b7fb45 100644 --- a/binaries/data/mods/public/simulation/components/Sound.js +++ b/binaries/data/mods/public/simulation/components/Sound.js @@ -1,5 +1,15 @@ function Sound() {} +Sound.prototype.Schema = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + Sound.prototype.Init = function() { }; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 70471eebc2..d401b9d5db 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -856,6 +856,19 @@ void Init(const CmdLineArgs& args, int flags) // (required for finding our output log files). g_Logger = new CLogger; + // Special command-line mode to dump the entity schemas instead of running the game. + // (This must be done after loading VFS etc, but should be done before wasting time + // on anything else.) + if (args.Has("dumpSchema")) + { + CSimulation2 sim(NULL, NULL); + sim.LoadDefaultScripts(); + std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); + f << sim.GenerateSchema(); + std::cout << "Generated entity.rng\n"; + exit(0); + } + // Call LoadLanguage(NULL) to initialize the I18n system, but // without loading an actual language file - translate() will // just show the English key text, which is better than crashing diff --git a/source/simulation2/Simulation2.cpp b/source/simulation2/Simulation2.cpp index c352c973fc..714dd988ea 100644 --- a/source/simulation2/Simulation2.cpp +++ b/source/simulation2/Simulation2.cpp @@ -306,3 +306,8 @@ bool CSimulation2::DeserializeState(std::istream& stream) // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } + +std::string CSimulation2::GenerateSchema() +{ + return m->m_ComponentManager.GenerateSchema(); +} diff --git a/source/simulation2/Simulation2.h b/source/simulation2/Simulation2.h index df1ea2e902..3bcd3014f2 100644 --- a/source/simulation2/Simulation2.h +++ b/source/simulation2/Simulation2.h @@ -128,6 +128,8 @@ public: bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); + std::string GenerateSchema(); + private: CSimulation2Impl* m; diff --git a/source/simulation2/components/CCmpFootprint.cpp b/source/simulation2/components/CCmpFootprint.cpp index 1222c1e31d..0227f9e641 100644 --- a/source/simulation2/components/CCmpFootprint.cpp +++ b/source/simulation2/components/CCmpFootprint.cpp @@ -36,6 +36,23 @@ public: CFixed_23_8 m_Size1; // height/radius CFixed_23_8 m_Height; + static std::string GetSchema() + { + return + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + } + virtual void Init(const CSimContext& UNUSED(context), const CParamNode& paramNode) { if (paramNode.GetChild("Square").IsOk()) diff --git a/source/simulation2/components/CCmpObstruction.cpp b/source/simulation2/components/CCmpObstruction.cpp index 2caf7d918c..b5a1f3d5fb 100644 --- a/source/simulation2/components/CCmpObstruction.cpp +++ b/source/simulation2/components/CCmpObstruction.cpp @@ -46,6 +46,13 @@ public: ICmpObstructionManager::tag_t m_Tag; + static std::string GetSchema() + { + return + "" + "" + ""; + } virtual void Init(const CSimContext& context, const CParamNode& paramNode) { m_Context = &context; diff --git a/source/simulation2/components/CCmpPosition.cpp b/source/simulation2/components/CCmpPosition.cpp index 233455a6e1..663f426cf8 100644 --- a/source/simulation2/components/CCmpPosition.cpp +++ b/source/simulation2/components/CCmpPosition.cpp @@ -75,27 +75,24 @@ public: bool m_Dirty; // true if position/rotation has changed since last TurnStart - /* - * Schema: (untested) - * - * - * - * - * - * upright - * pitch - * pitch-roll - * - * - * - * - * - * - * - * - * - * - */ + static std::string GetSchema() + { + return + "" + "" + "upright" + "pitch" + "pitch-roll" + "" + "" + "" + "" + "" + "" + "" + ""; + } + virtual void Init(const CSimContext& context, const CParamNode& paramNode) { m_Context = &context; diff --git a/source/simulation2/components/CCmpUnitMotion.cpp b/source/simulation2/components/CCmpUnitMotion.cpp index 4e17c12d06..a19017239f 100644 --- a/source/simulation2/components/CCmpUnitMotion.cpp +++ b/source/simulation2/components/CCmpUnitMotion.cpp @@ -56,6 +56,13 @@ public: }; int m_State; + static std::string GetSchema() + { + return + "" + "" + ""; + } virtual void Init(const CSimContext& context, const CParamNode& paramNode) { m_Context = &context; diff --git a/source/simulation2/components/CCmpVisualActor.cpp b/source/simulation2/components/CCmpVisualActor.cpp index eae6959f62..f372e6939e 100644 --- a/source/simulation2/components/CCmpVisualActor.cpp +++ b/source/simulation2/components/CCmpVisualActor.cpp @@ -60,6 +60,17 @@ public: { } + static std::string GetSchema() + { + return + "" + "" + "" + "" + "" + "" + ""; + } virtual void Init(const CSimContext& context, const CParamNode& paramNode) { if (!context.HasUnitManager()) @@ -68,7 +79,7 @@ public: // TODO: we should do some fancy animation of under-construction buildings rising from the ground, // but for now we'll just use the foundation actor and ignore the normal one std::string name; - if (paramNode.GetChild("Foundation").IsOk()) + if (paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk()) name = utf8_from_wstring(paramNode.GetChild("FoundationActor").ToString()); else name = utf8_from_wstring(paramNode.GetChild("Actor").ToString()); diff --git a/source/simulation2/system/Component.h b/source/simulation2/system/Component.h index a647f14053..831319a6ad 100644 --- a/source/simulation2/system/Component.h +++ b/source/simulation2/system/Component.h @@ -30,14 +30,14 @@ #define REGISTER_COMPONENT_TYPE(cname) \ void RegisterComponentType_##cname(CComponentManager& mgr) \ { \ - mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \ + mgr.RegisterComponentType(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \ CCmp##cname::ClassInit(mgr); \ } #define REGISTER_COMPONENT_SCRIPT_WRAPPER(cname) \ void RegisterComponentType_##cname(CComponentManager& mgr) \ { \ - mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname); \ + mgr.RegisterComponentTypeScriptWrapper(CCmp##cname::GetInterfaceId(), CID_##cname, CCmp##cname::Allocate, CCmp##cname::Deallocate, #cname, CCmp##cname::GetSchema()); \ CCmp##cname::ClassInit(mgr); \ } diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index fca8261d87..19a7039f81 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -185,8 +185,18 @@ void CComponentManager::Script_RegisterComponentType(void* cbdata, int iid, std: mustReloadComponents = true; } + std::string schema = ""; + { + CScriptValRooted prototype; + if (componentManager->m_ScriptInterface.GetProperty(ctor.get(), "prototype", prototype) && + componentManager->m_ScriptInterface.HasProperty(prototype.get(), "Schema")) + { + componentManager->m_ScriptInterface.GetProperty(prototype.get(), "Schema", schema); + } + } + // Construct a new ComponentType, using the wrapper's alloc functions - ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, ctor.get() }; + ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, ctor.get() }; componentManager->m_ComponentTypesById[cid] = ct; componentManager->m_CurrentComponent = cid; // needed by Subscribe @@ -365,17 +375,17 @@ void CComponentManager::ResetState() } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, - const char* name) + const char* name, const std::string& schema) { - ComponentType c = { CT_Native, iid, alloc, dealloc, name, 0 }; + ComponentType c = { CT_Native, iid, alloc, dealloc, name, schema, 0 }; m_ComponentTypesById.insert(std::make_pair(cid, c)); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, - DeallocFunc dealloc, const char* name) + DeallocFunc dealloc, const char* name, const std::string& schema) { - ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, 0 }; + ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, schema, 0 }; m_ComponentTypesById.insert(std::make_pair(cid, c)); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType @@ -715,3 +725,68 @@ void CComponentManager::SendGlobalMessage(const CMessage& msg) const } } } + + +std::string CComponentManager::GenerateSchema() +{ + std::string schema = + "" + "" + "0" + "" + "" + "0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + std::map > interfaceComponentTypes; + + for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) + { + schema += + "" + "" + "" + it->second.schema + "" + "" + ""; + + interfaceComponentTypes[it->second.iid].push_back(it->second.name); + } + + for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) + { + schema += ""; + std::vector& cts = interfaceComponentTypes[it->second]; + for (size_t i = 0; i < cts.size(); ++i) + schema += ""; + schema += ""; + } + + schema += + "" + "" + "" + ""; + for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) + schema += ""; + schema += + "" + "" + ""; + + schema += ""; + + // TODO: pretty-print + return schema; +} diff --git a/source/simulation2/system/ComponentManager.h b/source/simulation2/system/ComponentManager.h index 454eb883fb..52e94ba6a0 100644 --- a/source/simulation2/system/ComponentManager.h +++ b/source/simulation2/system/ComponentManager.h @@ -63,6 +63,7 @@ private: AllocFunc alloc; DeallocFunc dealloc; std::string name; + std::string schema; // RelaxNG fragment jsval ctor; // only valid if type == CT_Script }; @@ -82,8 +83,8 @@ public: void RegisterMessageType(MessageTypeId mtid, const char* name); - void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*); - void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*); + void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); + void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema); void SubscribeToMessageType(MessageTypeId); void SubscribeGloballyToMessageType(MessageTypeId); @@ -191,6 +192,8 @@ public: bool SerializeState(std::ostream& stream); bool DeserializeState(std::istream& stream); + std::string GenerateSchema(); + ScriptInterface& GetScriptInterface() { return m_ScriptInterface; } private: diff --git a/source/simulation2/system/IComponent.cpp b/source/simulation2/system/IComponent.cpp index a49b68137a..195b6982b2 100644 --- a/source/simulation2/system/IComponent.cpp +++ b/source/simulation2/system/IComponent.cpp @@ -23,6 +23,12 @@ IComponent::~IComponent() { } +std::string IComponent::GetSchema() +{ + // No schema specified -> allow only empty elements + return ""; +} + void IComponent::HandleMessage(const CSimContext& UNUSED(context), const CMessage& UNUSED(msg), bool UNUSED(global)) { } diff --git a/source/simulation2/system/IComponent.h b/source/simulation2/system/IComponent.h index b41cc58aa9..381fa63fcc 100644 --- a/source/simulation2/system/IComponent.h +++ b/source/simulation2/system/IComponent.h @@ -35,6 +35,8 @@ class IComponent public: virtual ~IComponent(); + static std::string GetSchema(); + virtual void Init(const CSimContext& context, const CParamNode& paramNode) = 0; virtual void Deinit(const CSimContext& context) = 0; diff --git a/source/tools/entvalidate/entvalidate.pl b/source/tools/entvalidate/entvalidate.pl new file mode 100644 index 0000000000..286e461409 --- /dev/null +++ b/source/tools/entvalidate/entvalidate.pl @@ -0,0 +1,154 @@ +use strict; +use warnings; + +use XML::Parser; +use XML::LibXML; +use Data::Dumper; +use Storable qw(dclone); +use File::Find; + +my $root = '../../../binaries/data/mods/public/simulation/templates'; +my $rngschema = XML::LibXML::RelaxNG->new(location =>'../../../binaries/system/entity.rng'); + +sub get_file +{ + my ($vfspath) = @_; + my $fn = "$root/$vfspath.xml"; + open my $f, $fn or die "Error loading $fn: $!"; + local $/; + return <$f>; +} + +sub trim +{ + my ($t) = @_; + return '' if not defined $t; + $t =~ /^\s*(.*?)\s*$/s; + return $1; +} + +sub load_xml +{ + my ($file) = @_; + my $root = {}; + my @stack = ($root); + my $p = new XML::Parser(Handlers => { + Start => sub { + my ($e, $n, %a) = @_; + my $t = {}; + die "Duplicate child node '$n'" if exists $stack[-1]{$n}; + $stack[-1]{$n} = $t; + for (keys %a) { + $t->{'@'.$_}{' content'} = trim($a{$_}); + } + push @stack, $t; + }, + End => sub { + my ($e, $n) = @_; + $stack[-1]{' content'} = trim($stack[-1]{' content'}); + pop @stack; + }, + Char => sub { + my ($e, $str) = @_; + $stack[-1]{' content'} .= $str; + }, + }); + $p->parse($file); + + return $root; +} + +sub apply_layer +{ + my ($base, $new) = @_; + $base->{' content'} = $new->{' content'}; + for my $k (grep $_ ne ' content', keys %$new) { + if ($new->{$k}{'@disable'}) { + delete $base->{$k}; + } else { + if ($new->{$k}{'@replace'}) { + delete $base->{$k}; + } + $base->{$k} ||= {}; + apply_layer($base->{$k}, $new->{$k}); + delete $base->{$k}{'@replace'}; + } + } +} + +sub load_inherited +{ + my ($vfspath) = @_; + my $layer = load_xml(get_file($vfspath)); + + if ($layer->{Entity}{'@parent'}) { + my $parent = load_inherited($layer->{Entity}{'@parent'}{' content'}); + apply_layer($parent->{Entity}, $layer->{Entity}); + return $parent; + } else { + return $layer; + } +} + +sub escape_xml +{ + my ($t) = @_; + $t =~ s/&/&/g; + $t =~ s//>/g; + $t =~ s/"/"/g; + $t =~ s/\t/ /g; + $t =~ s/\n/ /g; + $t =~ s/\r/ /g; + $t; +} + +sub to_xml +{ + my ($e) = @_; + my $r = $e->{' content'}; + $r = '' if not defined $r; + for my $k (sort grep !/^[\@ ]/, keys %$e) { + $r .= "<$k"; + for my $a (sort grep /^\@/, keys %{$e->{$k}}) { + $a =~ /^\@(.*)/; + $r .= " $1=\"".escape_xml($e->{$k}{$a}{' content'})."\""; + } + $r .= ">"; + $r .= to_xml($e->{$k}); + $r .= ""; + } + return $r; +} + +sub validate +{ + my ($vfspath) = @_; + my $xml = to_xml(load_inherited($vfspath)); + my $doc = XML::LibXML->new->parse_string($xml); + $rngschema->validate($doc); +} + +my @files; +sub find_process { + return $File::Find::prune = 1 if $_ eq '.svn'; + my $n = $File::Find::name; + return if /~$/; + return unless -f $_; + $n =~ s/\Q$root\///; + $n =~ s/\.xml$//; + push @files, $n; +} +find({ wanted => \&find_process }, $root); + +for my $f (sort @files) { + next if $f =~ /^template_/; + print "# $f...\n"; + eval { + validate($f); + }; + if ($@) { + print $@; + eval { print to_xml(load_inherited($f)), "\n"; } + } +}