Thank you for your suggestion, however the error still happens.

I also didn't made it clear, but the payload doesn't have a class to be reflected from, it can be abstract, requiring it to be parsed with %DynamicObject or %DynamicArray.

Since there's no class with %Stream.* property, %ConvertJSONToObject cannot decide what to do with long strings.
The ideal would be to detect the $$$MaxStringSize size and fallback to an internal %Stream instance before assigning the value to the proxy.

Also, I'm trying to avoid using %ZEN.proxyObject due to issues with stacking many objects for a single process. So the class parameter should be something inheriting from %DynamicAbstractObject.

 

Here's is the step-by-step:

DEV>set f = ##class(%Stream.FileCharacter).%New()
 
DEV>do f.LinkToFile("/temp/lorem.json")
 
DEV>set sc = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(f,, .o)
 
DEV>if $System.Status.IsError(sc) do $System.OBJ.DisplayError(sc)
 
ERRO #5002: Erro Caché: <MAXSTRING>%ParseJSON+290^%ZEN.Auxiliary.jsonProvider.1

This won't happen if I have long strings enabled.

Yes, like... %DynamicObject and %DynamicArray.

I still don't quite get why to use %objlasterror instead of returning the error.

Method %ToJSON(outstrm As %Stream.Object) As %String
{
if $D(outstrm) {
if $isobject(outstrm) {
if '(outstrm.%IsA("%Stream.Object")) {
throw ##class(%Exception.General).%New("%ToJSON - argument passed is not an instance of %Stream.Object")
}
try {
set ans = $zu(210,27,outstrm)
catch {
do  $$$APPERROR1($$$LASTERROR)  <- This appends the error to %objlasterror and that's it.
}

I see, we have been using an in-house tool with features similar to yours as well, at that time files were still being exported as XML instead of UDL. But now we're moving our development to local and enforcing project usage using this tool.

 

We also had our share of pain with editing live production code and I have to say, it's not the greatest feeling.

I don't know what you're extending your class from, but try calling ##super() before you set the timeout.

EDIT: Hold on, try using %response.Timeout instead. Maybe your problem is not the session itself, but the response timeout instead.

/// Can be set in the OnPreHTTP() method of a page in which case this changes the amount of time the
/// CSP gateway will wait for a response from the server in seconds before it reports the 'Server is not
/// responding' error message. This is useful if you know that this page is doing an expensive SQL
/// query that will take a couple of minutes and you want to set the server response timeout on
/// the CSP gateway to a minute and yet wait three minutes for this page to respond. It will just
/// change the server response timeout for this page only. If not set the the CSP gateway uses its
/// default timeout value specified in the CSP gateway configuration.
Property Timeout As %Integer;

That's quite a topic for complex discussions.

  • 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 plus repository issues.

  • 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?

Locally implemented and tested. tested and implemented.

  • Bonus question: How do you handle legacy code (and I mean the using lots of $ZUs kind of legacy) code? Leave it untouched and try to implement new features elesewhere? Rewrite the entire thing?

It depends, the more complex the code is, more I consider creating modern API wrappers instead of re-writting it.

Since booleans can be only true or false, you usually don't need to know about anything else, so let the caller handle what it should do.

 

ClassMethod AssertClassExists(ClassName As %String) As %Status
{
   if '..ClassExists(ClassName) return $$$ERRROR($$$ClassDoesNotExist, ClassName)
   return $$$OK
}

ClassMethod ClassExists(ClassName as %String) as %Boolean {

return ##class(%Dictionary.CompiledClass).%ExistsId(ClassName)

}

I only return the result when I'm absolutely sure that the method cannot throw any error. Otherwise I follow the rule:

1 - Obligatory arguments first.

2 - Result by second.

2 - Parameter with initial values third.

3 - Rest parameters for last.

(obligatoryParamA,  obligatoryParamB, obligatoryParamC, result,  optionalA, optionalB,  rest...)

Not sure if I understood what you meant, but I'll try to answer:

When using class methods you cannot reference your THIS instance. Have you every used Java, C# or whatever programming language that supports object oriented-programming paradigms? If so then class methods are the same as static methods.

Common (non-static) methods have the advantage of providing you the instance context. Which means that all properties are accessible inside this kind of method, even the private ones.

Class Sample.Person Extends %Persistent
{

Property Name As %String;

ClassMethod GetPersonName(instance As Sample.Person)
{
   return instance.Name // This works fine, GetPersonName is receiving an external Sample.Person.
}

// 'My' is used to empathize that this method refers to it's own instance name.
Method GetMyName() As %String 
{
   return ..Name // This also works fine.  Notice that this method doesn't receives the instance, it's because you can only call it from a instance already.
}

ClassMethod GetMyInstanceName() As %String
{
  return ...Name // This won't work. Since there's no context (instance). The compiler will even warn about it.
}
}

No, there isn't.

What he meant is that Caché supports binding for other languages. But there isn't a native COS implementation for PGP.
The easiest way of achieving what you want is to use gpg using $zf (CallOut). Where you can emit a comand directly to the host OS.

Remember that by default Windows haven't a gpg command. But there's a version for it as well.

You can also use method ##class(%Net.Remote.Utility).RunCommandViaZF(cmd,,.output,,) for brevity. Where cmd should be your gpg command.

The hard-coded offset does not come from a variable. A lack of comments in the code makes it very difficult to understand and debug.

No wonder...  Maybe you could use Studio or Atelier debugger to verify their values. Unless you need that log file of course.

Maybe you could also use break instead, just before the routine ends. And zwrite the context variables manually within the terminal.

I still don't know exactly what you want to achieve neither what are your limitations, I only know that you want to see the content from LINE, so I might be speaking something unnecessary.

I finally got it to work as I desired. Here's the source code, take a look on output routine:

This will:

Limit usage of ! to one per write.

Prevent initial write ! as I don't want to skip any line on the beginning.

Display compiler messages correctly.

Prevent from writing new lines for empty buffers.

Include portutils

Class Port.SourceControl.ExtendedHooks [ Abstract ]
{

ClassMethod Call(
sourceControl As %Studio.Extension.Base,
hookName As %String = "",
parameters... As %String) As %Status [ Internal, ProcedureBlock = 0 ]
{
  new sc, content, implementer, alreadyRedirected, currentMnemonic, childSC, expectingContent, firstLine
  
  set sc = $$$OK
  set childSC = $$$OK
  set implementer = ##class(Port.Configuration).GetExtendedHooksImplementer()
  set alreadyRedirected = ##class(%Device).ReDirectIO()
  set expectingContent = 0
  set firstLine = 0
  
  if '##class(%Dictionary.CompiledMethod).%ExistsId(implementer_"||"_hookName) return sc 
  set content = ##class(%Stream.GlobalBinary).%New()
  
  if implementer '= "" {
    write !, "Port: "_$$$FormatMsg("Port Log Messages", $$$RunningCustomHook, hookName, implementer)
    
    try {
      set currentMnemonic = "^"_##class(%Device).GetMnemonicRoutine()
      use $io::("^"_$zname)
      do ##class(%Device).ReDirectIO(1)
      set sc = $classmethod(implementer, hookName, sourceControl, parameters...)
    catch ex {
      set content = "" 
      set sc = ex.AsStatus()
    }
  }
  
  if alreadyRedirected 
    do ##class(%Device).ReDirectIO(1)
    use $io::(currentMnemonic)
  }
  
  if $isobject(content)
    do content.OutputToDevice()
  }
  
  write !
  
  if $$$ISOK(sc) {    
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedOK, hookName)
  else {
    set errorText = $System.Status.GetOneStatusText(sc)
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedError, hookName, errorText)
    set childSC = sc
    set sc = $$$PERROR($$$FailedWhileRunningExtendedHook, hookName)
    set sc = $$$EMBEDSC(sc, childSC)
  }   
  return sc 
  
rchr(c)
  quit
rstr(sz,to)
  quit
wchr(s)
  do output($char(s))
  quit 
wff()
  do output($char(12))
  quit
wnl()
  if firstLine = 0 set firstLine = 1
  else  set firstLine = -1
  do output($char(13,10))
  quit
wstr(s)
  do output(s)
  quit
wtab(s)
  do output($char(9))
  quit
output(s)
  // Skips writing the first !, we leave it to our write.
  if firstLine = 1 quit
  // Remaining writes ! are always a standalone buffer so we can check it's equality.
  if = $c(13,10) {
    // However we can only write if the the next buffer has indeed some content.
    // So we defer it to the next call where we can actually assert it.
    set expectingContent = 1
    // This catches writes with embedded CRLF (like the compiler ones).
  elseif $extract(s, 1, 2) = $c(13,10) {
    set expectingContent = 1
    do output($replace(s, $c(13,10), ""))
    set expectingContent = 0
    quit
  elseif $length(s) > 0 {
    // After deferring it, we can finally write a CRLF and the content, as long as it's not empty.
    if expectingContent = 1 {
      set expectingContent = 0
      do content.WriteLine()
      do content.Write($$$FormatText("Port (%1): ", hookName))
    }
    // Writes without ! must be written on the same line.
    do content.Write(s)
  }
   
  quit
}

}
 

  if $isobject(content) {
    do content.Rewind()
    while 'content.AtEnd set ^ck($i(i)) = content.ReadLine() }
    do content.OutputToDevice()
    do content.Rewind()
    do content.MoveTo(content.Size -3)
    do content.ReadLine(,,isEOL)
  }

^ck(1)="" <-- This is because write !, "First line"
^ck(2)="Port (OnAfterSave): First lineSecond line" <-- Though First line is actually pushed down and merged with "Second line"
^ck(3)="Port (OnAfterSave): Third line" <-- Third works fine, because second finishes with !
^ck(4)="Port (OnAfterSave): " <-- write "Third line", ! creates a blank line though my prefix is displaying.

I'm starting to think there's no way to prevent all situations.