Eduard Lebedyuk · Aug 28, 2017 go to post

Maybe you're sending HTML emails?

WriteLine works only for plaintext emails.

Use <br/> for new line in HTML emails.

To send HTML emails specify these settings:

set mail.ContentType = "text/html"
set mail.IsHTML = 1
set mail.Charset = "utf-8"
Eduard Lebedyuk · Aug 28, 2017 go to post

ID value contains all pieces of the primary key separated by ||. In the case of %Dictionary.StorageSQLMapDefinition it's:

Class||Storage||SQLMapName

So to open %Dictionary.StorageSQLMapDefinition you need to either construct an ID like this:

Set Class = "%CSP.Util.Performance"
Set Storage = "CspPerformance"
Set SQLMapName = "Map1"
Set Id = $LTS($LB(Class, Storage, SQLMapName), "||")
Set Obj = ##class(%Dictionary.StorageSQLMapDefinition).%OpenId(Id)

Alternatively you can use Open method for IDKEY index (more on auto-generated methods for indices, properties etc.):

Set Obj = ##class(%Dictionary.StorageSQLMapDefinition).IDKEYOpen(Class, Storage, SQLMapName)
Eduard Lebedyuk · Aug 28, 2017 go to post

There are several ways to handle this:

  1. Dynamically adjust BP settings (your suggestion). To do that you need to modify object(s) of Ens.Config.Item class and update the production. Check GetItemSettingValue method in Ens.Director class for an example of getting setting value. After that update the production and that's it. The problem with that approach are:
    • Updated settings take effect for all new incoming/outgoing messages
    • Updating production is a time-consuming operation, it should be used as rarely as possible
  2. Proxy process. Add proxy process that calls your target process. The proxy process should only contain logic related to error processing. And the target process should return the message immediately without any error checks.
  3. Modify the process so it does not return an error each time, but rather sleeps and calls the operation again depending on an error.

Code sample for 1 - utility method to search for setting in a production object.

/// Find BH in a current production by a setting value
ClassMethod findConfigItemBySettingValue(settingName As %String, settingValue As %String, businessType As %String = "", enabledOnly As %Boolean = 0) As Ens.Config.Item
{
    #dim sc As %Status
    
    // Get current production object
    #dim prod As Ens.Config.Production = ..getCurrentProduction()
    if '$isObject(prod) quit ""

    // Cycle over production elements
    #dim item As Ens.Config.Item
    #dim result As Ens.Config.Item = ""
    for i = prod.Items.Count():-1:1
    {
        set item = prod.Items.GetAt(i)
        
        // Search only for specified type
        if ((businessType '= "") && (item.BusinessType() '= businessType)) || (enabledOnly && 'item.Enabled) continue
        
        // Cycle over settings
        do item.PopulateModifiedSettings()
        set ind = ""
        for
        {
            set setting = item.ModifiedSettings.GetNext(.ind)
            if (ind = "") quit
            
            // Found it?
            if (setting.Name = settingName)
            {
                if ($ZStrip(setting.Value, "<>W") = $ZStrip(settingValue, "<>W")) set result = item
                quit
            }
        }
        
        if $isObject(result) quit
    }
    
    quit result
}

/// Get current running production object
ClassMethod getCurrentProduction() As Ens.Config.Production
{
    #dim sc As %Status
    #dim prodName As %String
    #dim prodState As %Integer
    
    // Find the name and status of current production
    set sc = ##class(Ens.Director).GetProductionStatus(.prodName, .prodState)
    if $$$ISERR(sc)
    {
        $$$LOGERROR($System.Status.GetErrorText(sc))
        quit ""
    }
    
    //Status should be "Running"
    if (prodState '= $$$eProductionStateRunning) quit ""
    
    // Open object by name
    #dim prod As Ens.Config.Production = ##class(Ens.Config.Production).%OpenId(prodName, , .sc)
    if $$$ISERR(sc)
    {
        $$$LOGERROR($System.Status.GetErrorText(sc))
        quit ""
    }
    
    quit prod
}
Eduard Lebedyuk · Aug 28, 2017 go to post

Why return binary data in JSON? I think it preferable to make a separate request for it.

Eduard Lebedyuk · Aug 15, 2017 go to post

SQL join or is there something more elaborate?

SELECT cd.name
FROM %Dictionary.ClassDefinition cd
JOIN subclassofquery sq ON sq.Name = cd.Name
WHERE cd.System IS NULL
Eduard Lebedyuk · Aug 15, 2017 go to post

Let's say you have this production definition:

Class Passthrough.Production Extends Ens.Production
{

XData ProductionDefinition
{
<Production Name="TEST" LogGeneralTraceEvents="false">
  <Description></Description>
  <ActorPoolSize>2</ActorPoolSize>
  <Item Name="PassthroughOperation" Category="" ClassName="EnsLib.SOAP.GenericOperation" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Adapter" Name="HTTPServer">www.webservicex.net</Setting>
    <Setting Target="Adapter" Name="URL">|</Setting>
  </Item>
  <Item Name="PassthroughService" Category="" ClassName="Passthrough.PassthroughService" PoolSize="0" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">PassthroughProcess</Setting>
    <Setting Target="Adapter" Name="EnableStandardRequests">1</Setting>
    <Setting Target="Adapter" Name="Port"></Setting>
    <Setting Target="Host" Name="OneWay">1</Setting>
  </Item>
  <Item Name="PassthroughProcess" Category="" ClassName="Passthrough.PassthroughProcess" PoolSize="1" Enabled="true" Foreground="false" Comment="" LogTraceEvents="false" Schedule="">
    <Setting Target="Host" Name="TargetConfigName">PassthroughOperation</Setting>
  </Item>
</Production>
}

You can change

After recompile, new name should be displayed in SMP.

Eduard Lebedyuk · Aug 14, 2017 go to post

Assuming you have %request object what happens when you call:

 S array=[].$fromJSON(%request.Content)

on small and big requests?

Eduard Lebedyuk · Aug 14, 2017 go to post

Seems like ability to parse files directly was introduced after 2016.1 (I tested in 2017.1 and it works. Your initial code works after replacing %From with $from:

 S filename="/tmp/pictures.json"
  S stream=##class(%Stream.FileCharacter).%New()
  S sc=stream.LinkToFile(filename)
  I ('sc) W "Error on linking file "_filename,! W
  try {
    set obj= [].$fromJSON(stream)
  } catch ex {
    w "Error. Unable to parse file "_filename,!
    w "Error type   "_ex.Name,!
    w "Error code   "_ex.Code,!
    w "Error location "_ex.Location,!
    set obj=""
  }
  q obj
  Q 

I would recommend upgrading from 2016.1 to 2017.1.

Eduard Lebedyuk · Aug 14, 2017 go to post

It's $fromJSON on 16.1:

Set filename = "/tmp/pictures.json"
Set array = [].$fromJSON(filename)
Eduard Lebedyuk · Aug 14, 2017 go to post

The problem is not with WSDL itself, but rather with the generated classes. There seems to be a loop dependency. Try recompiling.

Strategic placement of DependsOn keyword may help.

Eduard Lebedyuk · Aug 13, 2017 go to post

Missed it. Rebuild from management portal is equal to ##class(Package.Class).%BuildIndices(), except management portal rebuild is done asynchronously, so you need to go to SMP - Menu - Background Tasks to check that it's done.  ##class(Package.Class).%BuildIndices() is done synchronously so you know immediately that indices are rebuilt.

Eduard Lebedyuk · Aug 11, 2017 go to post
  1. Copy them into new array with keys = target property value. Iteration over this new array would be ordered as desired. Or when you set your array initially you can set keys = target property value.
  2. Another option is to order by local. Set some local with subscript = target property value, and local value as an oref. Then iterate over the local - you'll get ordered orefs.
Eduard Lebedyuk · Aug 10, 2017 go to post

1. How do you want to use this partial index?

2. NULL values are indexed, same as any other value (with one difference that any number of NULL values are allowed in  a Unique index). Consider the following example:

Class Sample.Person Extends %Persistent
{

Property Name As %String;

Index NameIndex On Name;

/// do ##class(Sample.Person).Test()
ClassMethod Test()
{
    kill ^Sample.PersonD, ^Sample.PersonI
    do ..AddPerson("ed")
    do ..AddPerson("ed")
    do ..AddPerson("bob")
    do ..AddPerson()
    do ..AddPerson()
    
    zw ^Sample.PersonD, ^Sample.PersonI
}

ClassMethod AddPerson(Name)
{
    set p = ..%New()
    set:$d(Name) p.Name = Name
    do p.%Save()
}

Whn I run the text method:

do ##class(Sample.Person).Test()

I get the following data global:

^Sample.PersonD=5
^Sample.PersonD(1)=$lb("","ed")
^Sample.PersonD(2)=$lb("","ed")
^Sample.PersonD(3)=$lb("","bob")
^Sample.PersonD(4)=$lb("","")
^Sample.PersonD(5)=$lb("","")

And the following index global:

^Sample.PersonI("NameIndex"," ",4)=""
^Sample.PersonI("NameIndex"," ",5)=""
^Sample.PersonI("NameIndex"," BOB",3)=""
^Sample.PersonI("NameIndex"," ED",1)=""
^Sample.PersonI("NameIndex"," ED",2)=""

As you see NULL values are indexed same as any other value.

Index global has the following structure:

^ClassIndexGlobal(IndexName, IndexValue, Id) = DataStoredInIndex

Where:

  • ClassIndexGlobal - is a global  name used for storing class globals. Defined in Storage.
  • IndexName-  is a name of the index
  • IndexValue - is a collated stored value (so "bob" becomes " BOB", and NULL  becomes " "). Some additional info on collation. Documentation.
  • Id - object id
  • DataStoredInIndex - any additional data stored in index. Documenatation.

Also for this SQL:

SELECT ID
FROM Sample.Person
WHERE Name IS NULL

The following plan that uses our index gets generated:

  • Read index map Sample.Person.NameIndex, using the given %SQLUPPER(Name), and looping on ID.
  • For each row:
    •  Output the row.
Eduard Lebedyuk · Aug 10, 2017 go to post

If you have a method that only returns dynamic object or array you can use [codemode = expression]:

ClassMethod TestGETMixedDynamicObject(class As Frontier.UnitTest.Fixtures.Class) As %DynamicObject(PUBLIC=1) [codemode=expression]
{
{
    "class": (class)
}
}
Eduard Lebedyuk · Aug 9, 2017 go to post

Use old json provider:

set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(json, class,.obj, $$$YES)

Instead of class argument, the json can contain _class property.

Eduard Lebedyuk · Aug 9, 2017 go to post

Settings are not really meant to be used like that.

If you want to report usage volume the best option would be creting a DeepSee cube over Ens.MessageHeader class. For a limited subset of analytic operations SQL may be enough.

Eduard Lebedyuk · Aug 8, 2017 go to post

Here's what I came up with.

Business service (works in SYNC or ASYNC mode depending on OneWay setting):

Class Passthrough.PassthroughService Extends EnsLib.SOAP.GenericService
{

Property DefaultResponce As %String(MAXLEN = "") [ InitialExpression = "<soap:Envelope><soap:Body></soap:Body></soap:Envelope>" ];

Parameter SETTINGS = "OneWay:Basic";

/// Pass through to OnProcessInput()
Method ProcessBody(pAction As %String, pRequestBody As %CharacterStream, pResponseBody As %CharacterStream) As %Boolean
{
    Set tSC=..ProcessInput(pRequestBody, .pResponseBody, pAction)
    Set:pResponseBody="" pResponseBody = ..DefaultResponce
    Quit $$$OK
}

}

And a BP for ASYNC logging (you need to set it as a target for BS only if you want ASYNC mode and logging, otherwise just call BO directly):

Class Passthrough.PassthroughProcess Extends Ens.BusinessProcess [ ClassType = persistent ]
{

/// Configuration item to which to send messages
Property TargetConfigName As Ens.DataType.ConfigName;

Parameter SETTINGS = "TargetConfigName:Basic:selector?multiSelect=0&context={Ens.ContextSearch/ProductionItems?targets=1&productionName=@productionId}";

Method OnRequest(pRequest As EnsLib.SOAP.GenericMessage, Output pResponse As EnsLib.SOAP.GenericMessage) As %Status
{
    Quit ..SendRequestSync(..TargetConfigName, pRequest, .pResponse)
}

}

Default EnsLib.SOAP.GenericOperation can be used for BO.

Eduard Lebedyuk · Aug 8, 2017 go to post

Have you thought about writing an article on External Service Registry  best practices? It seems like a very interesting topic.

Eduard Lebedyuk · Aug 8, 2017 go to post
  • Do you use an issue tracking / collaboration system? If so which one. Any you would recommend or immediately dismiss based on personal experience?

I use Github and GitLab. Issues are tracked there. They are fairly similar, use GitLab if you want on-premise solution.

  • How do you keep track of large code bases? Thousdands of folders named backup1, backups2, ..., SVN, git?

Git.

  • Do you have a development server to which you commit and test features there, or do you rather run a local copy of caché and implement features locally first, then push to the server?

Everything is implemented and tested locally. Then I push to a version control. Continuous integration does the rest.

Eduard Lebedyuk · Aug 8, 2017 go to post

I have modified Directory to point to a physical path, and restarted Caché. Still getting 404.

 

UPD. Adding trailing slash to directory name solved my problem.

Thank you.

Eduard Lebedyuk · Aug 7, 2017 go to post

Here's an example of calling class method with two parameters:

<call method="Method">
  <parameter expression="%request.Get("URLPAPAM")"/>
  <parameter value="123"/>
</call>

Check %ZEN.Report.call and %ZEN.Report.parameter for details.