Here's a solution that works for me:

s string="http://www.google.com"
set matcher=##class(%Regex.Matcher).%New("(\b(https?|ftp)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])",string)
w matcher.ReplaceAll("<a href='$1' target='_blank'>$1</a>")

Key changes:

  • Remove the enclosing / /gim - this is part of the regex syntax in JS.
  • Add two a-z ranges for case insensitivity
  • Remove JS escaping of slashes (not necessary)

I did, but I added it as a comment rather than an answer, so I can't mark it as the accepted answer. Regardless, I followed up this morning, and have been advised that the Management Portal's behavior is a bug and may be "fixed" in the future. The preferred solution would be additional configuration, either at the webserver level or adding more web applications.

Actually, this seems to just work (as part of a %CSP.REST subclass, used in a properly configured web application - the Management Portal UI disables the "CSP Files Physical Path" field if you enter a dispatch class, but will save it if you add the physical path first):

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<!-- ... various Routes/Forwards like in a typical REST API ... -->
<!-- Other routes: pass through to file system -->
<Route Url="/(.*)" Method="GET" Call="ServeStaticFile" />
</Routes>
}

ClassMethod ServeStaticFile(pPath As %String) As %Status
{
    #dim %request As %CSP.Request
    Do %request.Set("FILE",%request.Application_pPath)
    Quit ##class(%CSP.StreamServer).Page()
}

If there's nothing more to it than that... smiley

Looking at that documentation, one difference between LIST and GROUP_CONCAT is that GROUP_CONCAT lets you specify the separator, while LIST always uses a comma.

If you wanted to use a different separator, and your data will never contain commas, then it's as easy as (for example):

select home_city as "City", count(*) as "Count", REPLACE(LIST(Name),',',' ') as "Names"
from sample.person
group by home_city

If "your data will never contain commas" is a bad assumption (as it is in the case of Name in Sample.Person), the solution is to use %DLIST and $ListToString.

select home_city as "City", count(*) as "Count", $ListToString(%DLIST(Name),' ') as "Names"
from sample.person
group by home_city

%DLIST builds a $ListBuild list, and $ListToString joins the list elements with the specified separator.

%DLIST is useful in other cases too - for example, if your data might contain commas and you want to iterate over the aggregated data after running dynamic SQL.

One way to do this is comparing $listbuilds of the variable in question. Note also that you can coerce any value to a number with the "+" operator. So, for example:

USER>set var = 1
 
USER>w $lb(var)=$lb(+var)
1  <-- var is a number.
USER>set var = "1"
 
USER>w $lb(var)=$lb(+var)
0  <-- var is a not a number.

There's not really a simple built-in function to do exactly what you want though (at least that I know of).

It's defined in %cspInclude.inc as:

#define URLENCODE(%string) $zconvert($zconvert(%string,"O",$replace($$$GETIO,"JSML","UTF8")),"O","URL")

See documentation on $ZCONVERT as well. This mentions:

“UTF8” which converts (output mode) 16-bit Unicode characters to a series of 8-bit characters. Thus, the characters $CHAR(0) through $CHAR(127) are the same in RAW and UTF8 mode; characters $CHAR(128) and above are converted.

“URL” which adds (output mode) or removes (input mode) URL parameter escape characters to a string. Characters higher than $CHAR(255) are represented in Unicode hexadecimal notation: $CHAR(256) = %u0100.

So, in short, $$$URLENCODE converts the input from (possibly) 16-bit to 8-bit characters, then adds URL parameter escape characters where they're needed. (For example, escaping slashes.)

Another option to consider, if you have the flexibility to do so, would be putting an [IDKey] index on the code property of the code tables, having the code properties in ICDAutoCodeDefn refer to the code table classes rather than being of type %String, and then using implicit joins. I suspect (but haven't verified) that this would perform better than calculated/transient properties, and it's much easier to follow/maintain than normal JOINs.

Here's a full example. Looking at a general code table class:

Class DC.Demo.CodeTables.CodeTable Extends %Persistent [ Abstract, NoExtent ]
{
Index Code On Code [ IdKey ];
Property Code As %String;
Property Description As %String;
}

In this example, a specific code table would then extend that class, but don't need to add anything:

Class DC.Demo.CodeTables.Team Extends DC.Demo.CodeTables.CodeTable
{
}

Class DC.Demo.CodeTables.Position Extends DC.Demo.CodeTables.CodeTable
{
}

Then, in the class that refers to these code tables:

Class DC.Demo.CodeTables.Players Extends %Persistent
{
Property Name As %String(MAXLEN = 100);
Property Position As DC.Demo.CodeTables.Position;
Property Team As DC.Demo.CodeTables.Team;
ForeignKey Position(Position) References DC.Demo.CodeTables.Position();
ForeignKey Team(Team) References DC.Demo.CodeTables.Team();
}

(It's worth considering using foreign keys in cases like this.)

To demonstrate how this ends up working from an SQL perspective:

Class DC.Demo.CodeTables.Driver
{

ClassMethod Run()
{
    Do ##class(DC.Demo.CodeTables.Players).%KillExtent()
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')")
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')")
    Do ..ShowQueryResults("insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')")
    Do ..ShowQueryResults("select Name,Position->Description ""Position"",Team->Description ""Team"" from DC_Demo_CodeTables.Players")
}

ClassMethod ShowQueryResults(pQuery As %String, pParams...)
{
    Write !,"Running query: ",pQuery,!
    Do ##class(%SQL.Statement).%ExecDirect(,pQuery,pParams...).%Display()
}

}

The output from the Run() method is:

USER>Do ##class(DC.Demo.CodeTables.Driver).Run()

Running query: insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')
1 Row Affected
Running query: insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')
1 Row Affected
Running query: insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')
1 Row Affected
Running query: select Name,Position->Description "Position",Team->Description "Team" from DC_Demo_CodeTables.Players
Name    Position    Team
Rob Gronkowski    Tight End    New England Patriots

1 Rows(s) Affected

The Zen equivalent of ##super for JS is invokeSuper. Typically you'd just use:

this.invokeSuper('nameOfMethod',arguments);

All Zen components (including pages) have this. Here's an example from %ZEN.Component.dataCombo:

///  Set the value of a named property.<br>
ClientMethod setProperty(property, value, value2) [ Language = javascript ]
{
    switch(property) {
    case 'itemCount':
        break;
    case 'parameters':
        // set value of parameter: note that value will
        // be 1-based, so we have to convert it.
        // changing parameter always forces a query execution,
        // even in snapshot mode.
        if ('' != value) {
            value = value - 1;
            if (this.parameters[value]) {
                if (this.parameters[value].value != value2) {
                    this.parameters[value].value = value2;
                    this.clearCache();
                }
            }
        }
        break;
    default:
        // dispatch
        return this.invokeSuper('setProperty',arguments);
        break;
    }

    return true;
}

Hi Sebastian,

Sorry this went unanswered for so long. In case you haven't figured it out yet, one solution is:

    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)

%AddComponent registers the parameter with the page, so you won't get that error. (There might be a better way to do this, but %AddComponent at least seems to work.)

Here's a full example:

Class DC.Demo.DynamicComboPage Extends %ZEN.Component.page
{

Property count As %ZEN.Datatype.integer [ InitialExpression = 0 ];

/// This XML block defines the contents of this page.
XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" title="">
<button onclick="zenPage.AddCombo();" caption="Add a Combo Box!" />
</page>
}

Method AddCombo() [ ZenMethod ]
{
    #dim %page As DC.Demo.DynamicComboPage
    Set ..count = ..count + 1
    Set tParm = ##class(%ZEN.Auxiliary.parameter).%New()
    Set tParm.id = "parameter_"_..count
    Set tParm.name = "parameter_"_..count

    Set tExam = ##class(%ZEN.Component.dataCombo).%New()
    Set tExam.id = "dataCombo_"_..count
    Set tExam.label = "Test"
    
    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)
}

}

I think these error codes correspond to Windows system error codes - see this page.

So -2 corresponds to "ERROR_FILE_NOT_FOUND" and -3 to "ERROR_PATH_NOT_FOUND"

It seems more likely that you would get -2 from ##class(%File).Delete(file,.return). passing a directory that simply doesn't exist to RemoveDirectory results in -3. Is RemoveDirectory actually returning that error code, or perhaps a different one?

Certain configuration steps must be taken to enable access to % classes in non-/csp/sys/... web applications. Here's a link to the documentation about that.

Also, rather than creating your own % classes, you might consider either using the %All namespace (see documentation) or starting the package name with %Z. Starting the class's package name with %Z will prevent the class from being overwritten upon upgrade (which is important!) and, as a bonus, would also allow access from non-/csp/sys/ web applications automatically (according to the documentation at the first link).

Here's one way, using some macros from %occKeyword.inc/%occReference.inc (for tClass/tMethod/tLine in the above example):

Set tClsCode = $$$comMemberArrayGet(tClass,$$$cCLASSmethod,tMethod,$$$cMETHimplementation,tLine)

Note that this ends up looking at the class definition, which may not actually be the same as what's compiled. (However, used in this context, there are bigger problems in that case; it seems that the class->routine code mapping from %Studio.Debugger:SourceLine is invalidated if the class is changed but not compiled. This makes sense, but is definitely something to keep in mind if you're using it.)

I'm curious - what are you trying to do with this? smiley

There actually is a way to get this information that should work in many cases, using %Studio.Debugger:SourceLine, but it's not really easy. Note that %Studio.Debugger comes with the warning:

This class is used internally by Caché. You should not make direct use of it within your applications. There is no guarantee made about either the behavior or future operation of this class.

That said, here's a quick sample:

Class DC.Demo.SourceLine
{

ClassMethod Run()
{
    #; Here are a few comments to screw things up.
    Try {
        #; More comments!
        Write "Hello world",!
        Write 1/0
    } Catch e {
        Do ..HandleException(e)
    }
}

ClassMethod HandleException(pException As %Exception.AbstractException)
{
    Write "Exception occurred: ",pException.DisplayString(),!
    
    //Example value if e.Location (from above error): zRun+3^DC.Demo.SourceLine.1
    Set tSC = ..GetClassSourceLine(pException.Location,.tClsLocation)
    If $$$ISERR(tSC) {
        Write $System.Status.GetErrorText(tSC),!
    } else {
        Write ".INT error location: "_pException.Location,!
        Write ".CLS error location: "_tClsLocation,!
    }
}

ClassMethod GetClassSourceLine(pIntLocation As %String, Output pClsLocation As %String) As %Status
{
    Set tStatus = $$$OK
    Set pClsLocation = ""
    Try {
        Set tMethodAndLine = $Piece(pIntLocation,"^",1)
        Set tIntName = $Piece(pIntLocation,"^",2)
        Set tTag = $Piece(tMethodAndLine,"+")
        Set tRelativeOffset = $Piece(tMethodAndLine,"+",2)
        
        // Get routine text to find the absolute offset of tTag
        Set tTagOffset = 0
        Set tEndPos = 0
        Set tTextLines = 0
        For {
            Set tLine = $Text(@("+"_$Increment(tTextLines)_"^"_tIntName))
            Quit:tLine=""
            
            // Example:
            // zRun() public {
            // This relies on an assumption that methods will be sorted alphabetically and won't contain labels.
            If $Extract(tLine,1,$Length(tTag)) = tTag {
                Set tTagOffset = tTextLines //tTextLines is the counter.
                Set tEndPos = $Length(tLine)
                Quit
            }
        }
        
        // The absolute offset of the line in the .int file is the tag's offset plus the offset within it.
        Set tOffset = tTagOffset + tRelativeOffset
        Set tStatus = ##class(%Studio.Debugger).SourceLine(tIntName,tOffset,0,tOffset,tEndPos,,.tMap)
        If $$$ISERR(tStatus) {
            Quit
        }
        If $Data(tMap("CLS",1)) {
            Set $ListBuild(tClass,tMethod,tLine,tEndPos,tNamespace) = tMap("CLS",1)
            Set pClsLocation = tClass_":"_tMethod_"+"_tLine
        }
    } Catch e {
        Set tStatus = e.AsStatus()
    }
    Quit tStatus
}

}

And sample output:

USER>d ##class(DC.Demo.SourceLine).Run()
Hello world
Exception occurred: <DIVIDE> 18 zRun+3^DC.Demo.SourceLine.1
.INT error location: zRun+3^DC.Demo.SourceLine.1
.CLS error location: DC.Demo.SourceLine:Run+5

If an exception occurs in generated code, I'd expect this to be useless.

There isn't a built-in method that does exactly what you're looking for, that I know of. Here's a simple example of how to do it, though:

Class DC.Demo.ArrayUtils
{

ClassMethod ArrayObjectToArray(pSource As %Collection.AbstractArray, Output pTarget)
{
    Kill pTarget
    
    Set tKey = ""
    For {
        Set tItem = pSource.GetNext(.tKey)
        Quit:tKey=""
        Set pTarget(tKey) = tItem
    }
}

ClassMethod ArrayToArrayObject(ByRef pSource, pTarget As %Collection.AbstractArray)
{
    // Could initialize pTarget here and return it Output, or return it normally.
    // This is just a bit more general because it'll work with any array collection type.
    Do pTarget.Clear()
    
    Set tKey = ""
    For {
        Set tKey = $Order(pSource(tKey),1,tItem)
        Quit:tKey=""
        Do pTarget.SetAt(tItem,tKey)
    }
}

}

Usage:

USER>s aodt=##class(%ArrayOfDataTypes).%New()
 
USER>w aodt.SetAt("lcavanaugh","username")
1
USER>w aodt.SetAt("organization","coolcompany")
1
USER>zw aodt
aodt=<OBJECT REFERENCE>[1@%Library.ArrayOfDataTypes]
+----------------- general information ---------------
|      oref value: 1
|      class name: %Library.ArrayOfDataTypes
| reference count: 2
+----------------- attribute values ------------------
|Data("coolcompany") = "organization"
|   Data("username") = "lcavanaugh"
|        ElementType = "%String"
+-----------------------------------------------------
 
USER>do ##class(DC.Demo.ArrayUtils).ArrayObjectToArray(aodt,.array)
 
USER>zw array
array("coolcompany")="organization"
array("username")="lcavanaugh"
 
USER>s newaodt = ##class(%Library.ArrayOfDataTypes).%New()
 
USER>do ##class(DC.Demo.ArrayUtils).ArrayToArrayObject(.array,newaodt)
 
USER>zw newaodt
newaodt=<OBJECT REFERENCE>[2@%Library.ArrayOfDataTypes]
+----------------- general information ---------------
|      oref value: 2
|      class name: %Library.ArrayOfDataTypes
| reference count: 2
+----------------- attribute values ------------------
|Data("coolcompany") = "organization"
|   Data("username") = "lcavanaugh"
|        ElementType = "%String"
+-----------------------------------------------------

If you really wanted to make BuildValueArray work:

USER>d aodt.%SerializeObject(.serial)
 
USER>d aodt.BuildValueArray($lg(serial),.anotherarray)
 
USER>zw anotherarray
anotherarray("coolcompany")="organization"
anotherarray("username")="lcavanaugh"

The utility method approach would probably at least be clearer. smiley