Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- #996: Ensure COS commands execute in exec under a dedicated, isolated context
- #1013: Implement recursive placeholder resolution in Default parameters

## [0.10.5] - 2026-01-15

Expand Down
1 change: 1 addition & 0 deletions src/cls/IPM/Storage/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,7 @@ Method %Evaluate(
set customParams("verbose") = +$get(pParams("Verbose"))
set customParams("ipmDir") = ##class(%Library.File).NormalizeDirectory($system.Util.DataDirectory() _ "ipm/" _ ..Name _ "/" _ ..VersionString)
set tAttrValue = ##class(%IPM.Utils.Module).%EvaluateMacro(tAttrValue)
do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams)
set tAttrValue = ##class(%IPM.Storage.ModuleSetting.Default).EvaluateAttribute(tAttrValue,.customParams)
set attrValue = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(tAttrValue)

Expand Down
52 changes: 52 additions & 0 deletions src/cls/IPM/Storage/ModuleSetting/Default.cls
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,58 @@ ClassMethod EvaluateAttribute(
return attribute
}

/// Processes configuration values to resolve variable references.
/// Searches for placeholders using the ${var} or {$var} syntax and replaces them with
/// the value of the corresponding 'Default' or 'Resource' name.
ClassMethod ResolvePlaceholders(ByRef customParams)
{
set found = 1
// maxLevels is a safety guard to prevents infinite loops caused by circular references.
set maxLevels = 20
do ##class(%IPM.Utils.Module).GetSystemExpressions(.systemPrams)

while (found && (maxLevels > 0)) {
set found = 0
//Decrement levels and check for circular references
if '$increment(maxLevels, -1) {
$$$ThrowOnError($$$ERROR($$$GeneralError,"Circular reference or too many levels in placeholders"))
}
set param = ""
for {
set param = $order(customParams(param), 1, data)
quit:param=""
//Skip if no placeholders remain
continue:data'["{"

set initialData = data
for delimiter = "${", "{$" {
continue:data'[delimiter
set pCount = $length(data, delimiter)
for i=2:1:pCount {
kill seen
set chunk = $piece(data, delimiter, i)
set key = $piece(chunk, "}", 1)
continue:key=""
continue:$data(seen(key))
set seen(key) = ""

set search = delimiter _ key _ "}"
if $data(customParams(key), val) {
set data = $replace(data, search, val)
} elseif $data(systemPrams($$$lcase(key)), val) {
// system variables handled as Case-Insensitive
set data = $replace(data, search, val)
}
}
}
if data '= initialData {
set customParams(param) = data
set found = 1
}
}
}
}

Storage Default
{
<Data name="DefaultState">
Expand Down
20 changes: 20 additions & 0 deletions src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2349,6 +2349,26 @@ ClassMethod %EvaluateSystemExpression(pString As %String) As %String [ Internal
return result
}

ClassMethod GetSystemExpressions(ByRef SystemParams) As %String [ Internal ]
{
do ..GetDatabaseInfoForNamespace($namespace, .properties)
set SystemParams("namespace") = $namespace
set SystemParams("namespacelower") = $zconvert($namespace,"L")
set SystemParams("namespaceroutinedb") = $get(properties("Routines"))
set SystemParams("namespaceglobalsdb") = $get(properties("Globals"))
set SystemParams("installdir") = $system.Util.InstallDirectory()
set SystemParams("datadir") = $system.Util.DataDirectory()
set SystemParams("mgrdir") = ##class(%Library.File).ManagerDirectory()
set SystemParams("cspdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"csp")
set SystemParams("bindir") = $system.Util.BinaryDirectory()
set SystemParams("libdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"lib")
do ##class(%Studio.General).GetWebServerPort(,,,.urlRoot)
set SystemParams("webroot") = urlRoot
set dbRole = ..GetDatabaseRole()
set SystemParams("dbrole") = dbRole
set SystemParams("globalsbbrole") = dbRole
}

ClassMethod %EvaluateMacro(pString As %String) As %String [ Internal ]
{
if pString '[ "$$$" {
Expand Down
152 changes: 152 additions & 0 deletions tests/integration_tests/Test/PM/Integration/Module.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
Class Test.PM.Integration.Module Extends Test.PM.Integration.Base
{

Parameter CommonPathPrefix As STRING = "varresolver";

Parameter ModuleName As STRING = "demo-module1";

Parameter ClsDefName As STRING = "Test.TrackPlaceHolders";

/// This test validates that the IPM engine can handle "chained" ${variables}
/// (placeholders that resolve to other placeholders).
/// 1. Generates a 'module.xml' from XData with deep variable dependencies.
/// 2. Executes IPM Shell 'load' to verify multi-pass expansion.
/// 3. Ensures no unresolved placeholders remain after the load process.
Method TestNestedPlaceholderVar()
{
do $$$LogMessage("Create '"_..#ClsDefName_"' class definition and method 'CaptureResolvedPlaceHolders' to capturing the placeholder variables")
set status = ..CreateClassdef()
do $$$AssertStatusOK(status,"Class definition created successfully")

do $$$LogMessage(" start Loading the "_..#ModuleName_" module")
set status = ..CreateModuleXml(.moduleDir)
do $$$AssertStatusOK(status,"Created the xml file on "_moduleDir)

set status = ##class(%IPM.Main).Shell("load "_moduleDir)
do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully from "_moduleDir)

do $$$LogMessage("Validate the place holder variable resolved values in ^IRIS.Temp.IPMVarTest global")
set data = $get(^IRIS.Temp.IPMVarTest("frankenstein"))
if data'="" {
do $$$AssertTrue(1,"frankenstein place holder value is resolved to "_data)
}
do $$$LogMessage("Verifying other parameter values")
do $$$LogMessage(" ^IRIS.Temp.IPMVarTest set on Namespace: "_$namespace)
set sub=""
for {
set sub = $order(^IRIS.Temp.IPMVarTest(sub),1,data)
quit:sub=""
do $$$LogMessage("^IRIS.Temp.IPMVarTest("_sub_") and value : "_data)
}
set module = ##class(%IPM.Storage.Module).NameOpen(..#ModuleName)
do $$$AssertTrue($isobject(module), "Module "_..#ModuleName_" exists in IPM and version is "_ module.Version.ToString())

do $$$LogMessage("List all modules")
set status = ##class(%IPM.Main).Shell("list")
do $$$AssertStatusOK(status,"List all modules")

set status = ##class(%IPM.Main).Shell("uninstall "_..#ModuleName)
do $$$AssertStatusOK(status,"uninstalled module "_..#ModuleName_" successfully.")

set status = ##class(%File).Delete(##class(%File).NormalizeFilename("module.xml",moduleDir))
do $$$AssertStatusOK(status,"Deleted the module.xml file from "_moduleDir)

do $$$LogMessage("Deleting the "_..#ClsDefName_" generated test class")
set status = ##class(%Dictionary.ClassDefinition).%DeleteId(..#ClsDefName)
do $$$AssertStatusOK(status,"class "_..#ClsDefName_" is deleted successfully")

do $$$LogMessage("Deleting the ^IRIS.Temp.IPMVarTest global")
kill ^IRIS.Temp.IPMVarTest
}

/// Create class definition at runtime to capute the resovled reference value through <invoke> in manifest
ClassMethod CreateClassdef() As %Status
{
set cls = ##class(%Dictionary.ClassDefinition).%New()
set cls.Name = ..#ClsDefName
// create sample method
set method = ##class(%Dictionary.MethodDefinition).%New()
set method.Name = "CaptureResolvedPlaceHolders"
set method.ClassMethod = 1
set method.FormalSpec = "frankenstein,args..."
do method.Implementation.WriteLine($char(9)_"set ^IRIS.Temp.IPMVarTest(""frankenstein"")= frankenstein")
do method.Implementation.WriteLine($char(9)_"merge ^IRIS.Temp.IPMVarTest = args")
do cls.Methods.Insert(method)
set sc = cls.%Save()
do $system.OBJ.Compile(..#ClsDefName)
return sc
}

Method CreateModuleXml(Output pModuleDir) As %Status
{
#define NormalizeDirectory(%path) ##class(%File).NormalizeDirectory(%path)
#define UTRoot ^UnitTestRoot

set sc = 1
set testRoot = $$$NormalizeDirectory($get($$$UTRoot))
set pModuleDir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/")

if '##class(%File).DirectoryExists(pModuleDir) {
set sc = ##class(%File).CreateDirectoryChain(pModuleDir)
}
do $$$AssertStatusOK(sc,"Directory created "_pModuleDir)
set stream = ##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||TestModuleXML").Data
set fileStream = ##class(%Stream.FileBinary).%New()
set fileStream.Filename=##class(%File).NormalizeFilename("module.xml",pModuleDir)
set sc = fileStream.CopyFromAndSave(stream)
do $$$AssertStatusOK(1,"module.xml File created on "_pModuleDir)

return sc
}

/// Sample module file
XData TestModuleXML [ MimeType = application/xml ]
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="demo-module1.ZPM">
<Module>
<Name>demo-module1</Name>
<Version>1.0.0</Version>
<Description>testing the name resolved</Description>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<Default Name="dataversion" Value="${version}/" />
<Default Name="datadefaultmgrdir" Value="${mgrdir}/" />
<Default Name="mTestPlaceHolder" Value="${dataDefaultTest2}/xtest/${version}/${mgrdir}/${mTestVersion}/${datapath1}" />
<Default Name="datadefaultcspdir" Value="${cspdir}/" />
<Default Name="dataDefaultTest2" Value="${datadefaultcspdir}/xdata" />
<Default Name="dataDefaultTest3" Value="${dataDefaultTest2}/xtest" />
<Default Name="mTestVersion" Value="${dataDefaultTest2}/xtest/${version}" />
<Default Name="ipmtest" Value="TESTING MY STRING"/>
<Default Name="ipmdir" Value="/usr/irissys/mgr/user/mts"/>
<Default Name="datapath" Value="${libdir}data/" />
<Default Name="datapath1" Value="${ipmdir}data/" />

<Default Name="start" Value="${" />
<Default Name="middle" Value="namespace" />
<Default Name="end" Value="}" />
<Default Name="frankenstein" Value="${start}${middle}${end}"/>
<SystemRequirements Version=">=2020.1" Interoperability="enabled"/>
<SourcesRoot>src</SourcesRoot>
<Invoke Class="Test.TrackPlaceHolders" Method="CaptureResolvedPlaceHolders">
<Arg>${frankenstein}</Arg>
<Arg>${dataversion} </Arg>
<Arg>"12121212" </Arg>
<Arg>"12121212ASHOK"</Arg>
<Arg>${datadefaultcspdir}</Arg>
<Arg>${dataDefaultTest2}</Arg>
<Arg>${dataDefaultTest3} </Arg>
<Arg>${mTestVersion}</Arg>
<Arg>${ipmtest}</Arg>
<Arg>${datapath1}</Arg>
<Arg>${ipmdir} </Arg>
<Arg>${datadefaultmgrdir}</Arg>
</Invoke>
<AfterInstallMessage>Module installed successfully!</AfterInstallMessage>
</Module>
</Document>
</Export>
}

}
55 changes: 55 additions & 0 deletions tests/unit_tests/Test/PM/Unit/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,59 @@ Method TestFixUndefinedCLIGenCommand()
do $$$AssertStatusOK(sc, "AddWebApps method must now process web app list without error.")
}

Method TestResolveAllVariables()
{
//Setup Test Data
set customParams("count") = 7
set customParams("version") = "1.0.0"
set customParams("datadefaultcspdir") = "${cspdir}"
set customParams("datadefaultmgrdir") = "${mgrdir}"
set customParams("dataversion") = "${version}"
set customParams("dataDefaultTest2") = "${datadefaultcspdir}xdata"
set customParams("dataDefaultTest3") = "${dataDefaultTest2}xtest"
set customParams("datapath") = "${libdir}data/"
set customParams("ipmdir") = "/usr/irissys/mgr/user/mts"
set customParams("datapath1") = "${ipmdir}data/"
set customParams("mTestVersion") = "${dataDefaultTest2}/xtest/${version}"
set customParams("mTestPlaceHolder") = "${dataDefaultTest2}/xtest/${version}${mgrdir}${mTestVersion}${datapath1}"

// Frankenstein Variable Assembly
// These variables test the "Multi-Pass" capability of the resolver.
// It must first resolve 'start', 'middle', and 'end' to build a new
// valid placeholder string ("${namespace}"), and then resolve that
// string against the System Dictionary in a subsequent pass.
set customParams("start") = "${"
set customParams("middle") = "namespace"
set customParams("end") = "}"
set customParams("frankenstein") = "${start}${middle}${end}"

merge customParamsIn = customParams
// output
set customParamsOut("count")=7
set customParamsOut("dataDefaultTest2")="/usr/irissys/csp/xdata"
set customParamsOut("dataDefaultTest3")="/usr/irissys/csp/xdataxtest"
set customParamsOut("datadefaultcspdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${cspdir}")
set customParamsOut("datadefaultmgrdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${mgrdir}")
set customParamsOut("datapath")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${libdir}")_"data/"
set customParamsOut("datapath1")="/usr/irissys/mgr/user/mtsdata/"
set customParamsOut("dataversion")="1.0.0"
set customParamsOut("ipmdir")="/usr/irissys/mgr/user/mts"
set customParamsOut("mTestPlaceHolder")="/usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr//usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr/user/mtsdata/"
set customParamsOut("mTestVersion")="/usr/irissys/csp/xdata/xtest/1.0.0"
set customParamsOut("version")="1.0.0"
set customParamsOut("start") = "${"
set customParamsOut("middle") = "namespace"
set customParamsOut("end") = "}"
set customParamsOut("frankenstein") = "USER"
do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams)

do $$$LogMessage("Validate all placholder variables")
set variable=""
for {
set variable = $order(customParams(variable))
quit:variable=""
do $$$AssertEquals(customParams(variable), customParamsOut(variable), variable_" is resolved correctly and the placeholder "_customParamsIn(variable)_" value is "_customParamsOut(variable))
}
}

}