Eduard Lebedyuk · Mar 14, 2019 go to post

The easiest way would be to configure web server on customersdomain.com to act as a reverse proxy. In this case it would proxy requests to yourownserver.com or wherever you need.

Another way is to install CSP Gateway on a customersdomain.com. After that connect CSP Gateway to your Cache/Ensemble/InterSystems IRIS instance on yourownserver.com. And connect web server on customersdomain.com to CSP Gateway on customersdomain.com.

Advantage of this approach is that static would be served directly from customersdomain.com server.

Eduard Lebedyuk · Mar 14, 2019 go to post

Can you try restarting the instance?

CSP absolutely should work.

Also check this post about clearing cached elements from webserver.

Eduard Lebedyuk · Mar 13, 2019 go to post

I never use return unless I really do want to exit from the method while inside some inner loop. So mainly I use quit.

Eduard Lebedyuk · Mar 12, 2019 go to post

-Creating SSL/TLS configurations in S1's Healthshare portal (also tried with a %SuperServer... but where and how could I use them ? I haven't found it)

Your SSL configuration should be called %SuperServer. Currently it's called AccDirSsl. You need to create new/rename existing configuration to %SuperServer.

Also, can you show a screen from the Portal’s System-wide Security Parameters page (System Administration > Security > System Security > System-wide Security Parameters)? For the Superserver SSL/TLS Support choice, you should select Enabled (not Required).

Also does HS OS user has access to C:\chr11614pem? I'd try to copy certificates/keys to HS temp directory and modify paths in config accordingly.

Eduard Lebedyuk · Mar 11, 2019 go to post

From docs. RETURN and QUIT differ when issued from within a FOR, DO WHILE, or WHILE flow-of-control structure, or a TRY or CATCH block.

  • You can use RETURN to terminate execution of a routine at any point, including from within a FOR, DO WHILE, or WHILE loop or nested loop structure. RETURN always exits the current routine, returning to the calling routine or terminating the program if there is no calling routine. RETURN always behaves the same, regardless of whether it is issued from within a code block. This includes a TRY block or a CATCH block.
  • In contrast, QUIT exits only the current structure when issued from within a FOR loop, a DO WHILE loop, a WHILE loop, or a TRY or CATCH block. QUIT exits the structure block and continues execution of the current routine with the next command outside of that structure block. QUIT exits the current routine when issued outside of a block structure or from within an IF, ELSEIF, or ELSE code block.
Eduard Lebedyuk · Mar 11, 2019 go to post

If you already have HL7 message inside Ensemble you can use  Caché SQL Gateway which provides access from Caché to external databases via JDBC and ODBC. You can use SQL Gateway (probably in ODBC mode) to update SQL Server table(s).

Eduard Lebedyuk · Mar 9, 2019 go to post

You can try to write to a TCP device with SSL. Doesn't require additional permissions:

ClassMethod Exists(ssl As %String) As %Boolean
{
	#dim exists As %Boolean = $$$YES
	set host = "google.com"
	set port = 443
	set timeout = 1
	
	set io = $io
	
	set device = "|TCP|" _ ##class(%PopulateUtils).Integer(5000, 10000)
	try {
		open device:(host:port:/SSL=ssl):timeout	
		use device
		
		// real check
		write "GET /" _ $c(10),*-3
		// real check - end
		
		// should be HTTP/1.0 200 OK but we don't really care
		//read response:timeout
		//write response
	} catch ex {
		set exists = $$$NO
	}
	
	use io
	
	close device
	
	
	quit exists
}

It's slower than direct global check but if you want to do it rarely,  I think it could be okay. Doesn't require additional permissions.

Code to compare times:


ClassMethod ExistGlobal(ssl) [ CodeMode = expression ]
{
$d(^|"%SYS"|SYS("Security","SSLConfigsD",ssl))#10
}

/// do ##class().Compare()
ClassMethod Compare(count = 1, ssl = "GitHub")
{
    Write "Iterations: ", count,!
    Write "Config exists: ", ..Exists(ssl),!

    set start = $zh
    for i=1:1:count {
        set exists = ..Exists(ssl)
    }
    
    set end = $zh
    
    set time = end - start
    Write "Device check: ", time,!
    
    set start = $zh
    for i=1:1:count {
        set exists = ..ExistGlobal(ssl)
    }
    
    set end = $zh
    
    set time2 = end - start
    write "Global check: ", time2,!
}

Results:

Iterations: 1
Config exists: 1
Device check: .054983
Global check: .000032

Iterations: 1
Config exists: 0
Device check: .017351
Global check: .00001

Iterations: 50
Config exists: 1
Device check: 2.804497
Global check: .000097

Iterations: 50
Config exists: 0
Device check: .906424
Global check: .000078
Eduard Lebedyuk · Mar 9, 2019 go to post

You can use negative integers to subtract hours. DATEADD also words with timestamps:

write $SYSTEM.SQL.DATEADD("hour", -3, "2019-03-09 10:00:00")
>2019-03-09 07:00:00
Eduard Lebedyuk · Mar 9, 2019 go to post

I can use ($ztimestamp) to get UTC time and then convert it into local time i am using the below way is this correct?

 SET stamp=$ZTIMESTAMP
w !,stamp
SET localutc=$ZDATETIMEH(stamp,-3)
w $ZDATETIME(localutc,3,1,2)

Yes, sure.

My Question is how i can program this task in the below way

You heed to add three hours.  Use DATEADD method for this:

write $SYSTEM.SQL.DATEADD("hour", 3, yourDate)
Eduard Lebedyuk · Mar 8, 2019 go to post

IRIS Text Analytics/iKnow and Analytics/DeepSee are enabled on per-application basis. Interoperability/Ensemble/HealthShare are enabled on a per-namespace basis.

First you need to get default application from the namespace:

set namespace = "USER"
set app = $System.CSP.GetDefaultApp(namespace) _ "/"

And then call one of these methods:

do EnableIKnow^%SYS.cspServer(app)
do EnableDeepSee^%SYS.cspServer(app)

If you want to enable Interoperability/Ensemble call:

set sc = ##class(%EnsembleMgr).EnableNamespace(namespace,1)
Eduard Lebedyuk · Mar 7, 2019 go to post

You can do that in Analyzer.

Choose the row/column you want displayed this way, click on it's settings and set italic header:

Eduard Lebedyuk · Mar 7, 2019 go to post

You can use CreateDirectoryChain method of %File class to create directory tree instead of several calls to CreateDirectory.

Eduard Lebedyuk · Mar 7, 2019 go to post

Link to file doesn't need for a file to exist, but the containing directory must exist and should be writable by a OS user (cacheusr in uyour case probably).

I'd try to write into a temp dir first, where you're sure you have access:

set file = ##class(%File).TempFilename("pdf")
set sc = stream2.LinkToFile(file)
quit:$$$ISERR(sc) sc

record value of file somewhere (output to display or store in global) and check if the file was created).

%Save method also returns status, you should return it instead of $$$OK:

set sc = stream2.%Save()
quit sc
Eduard Lebedyuk · Mar 7, 2019 go to post

The code looks good. What error are you getting?

Try replacing stream2 %Stream.FileCharacter with %Stream.FileBinary.

Try replacing stream1 %Stream.FileCharacter with %Stream.TmpBinary.

If you have message sample less than 3,5 mb in size try to write a test without intermediate stream.

You probably should write to a pdf file and not a txt one.

If you can, get a sample original/decoded file. Compare original file and your stream2 file using hex editor to spot differences.

Eduard Lebedyuk · Mar 7, 2019 go to post

I try to separate SQL from ObjectScript code. So mainly, I'm using queries, for example:

/// Some report. To display in terminal call:
/// do ##class(class).reportFunc().%Display()
Query report(date As %String = {$zd($h-1,8)}) As %SQLQuery(SELECTMODE = "ODBC")
{
SELECT
  ID,
  Value,
  EventDate
FROM myTable
WHERE EventDate>=TO_POSIXTIME(:date,'YYYYMMDD') AND EventDate<TO_POSIXTIME(:date+1,'YYYYMMDD')
ORDER BY EventDate
}

Queries can be calles from ObjectScript using autogenerated Func method:

/// Really %sqlcq.<NAMESPACE>.cls<NUMBER>
#dim rs As %SQL.ISelectResult
set rs = ..reportFunc(date)
//do rs.%Display()

while rs.%Next() {
    write rs.ID,!
}

I found this approach improves readability of the codebase. More about queries in this article.

Special case - one value.

Sometimes you don't need a resultset, but one value. In that case:

If you know ID it's possible to use GetStored method:

set value = ##class(test).<PropertyName>GetStored(ID)

If you know unique indexed value but don't know ID, it's possible to get id with Exists method:

ClassMethod <IndexName>Exists(val, Output id) As %Boolean

And after that use GetStored method.

More on auto-generated methods, such as GetStored and Exists in this article.

Finally, if you can't use above methods or you need one value but it's an aggregate, use embedded SQL if it's a short SQL and Query if it's long.

Eduard Lebedyuk · Mar 7, 2019 go to post

Use methods from %CSP.Portal.Utils class:

set pageID = "" //page url, URL encoded via $zconvert(url,"O","URL")

set currentResource = ##class(%CSP.Portal.Utils).%GetCustomResource(pageID)

set sc = ##class(%CSP.Portal.Utils).%SetCustomResource(pageID, newResource)

Eduard Lebedyuk · Mar 7, 2019 go to post

I' do it in 2 steps.

  1. Iterate over data array and build a temp local structure to hold all additional items you need
  2. Iterate over this new structure and add these items to data array.

You can add to array using %Push method:

do Obj.data.%Push(newItem)

If you want to push at a specific posiiton, use %Set

do Obj.data.%Set(position, newItem)

That said your later structure contains data which does not exist in the original structure (text values for projects and sub-projects) so you need to get it from somewhere.

Also is project - subproject hierarchy one-level, or there could be an arbitrary number of sub-project levels (i.e. 10  10-1 → 10-1-1 → 10-1-1-1)?

Eduard Lebedyuk · Mar 7, 2019 go to post

Enable ODBC log. Maybe there would be errors pointing to the root of the issue?

Maybe something to do with INFORMATION_SCHEMA.

Eduard Lebedyuk · Mar 5, 2019 go to post

Okay, even if the classes/globals are the same there's a solution. Let's say you have Sample.Person class in namespaces SAMPLES and USER, each with their own data:

Class Sample.Person Extends %Persistent
{

Property Name;

Storage Default
{
<Data name="PersonDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^Sample.PersonD</DataLocation>
<DefaultData>PersonDefaultData</DefaultData>
<IdLocation>^Sample.PersonD</IdLocation>
<IndexLocation>^Sample.PersonI</IndexLocation>
<StreamLocation>^Sample.PersonS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}


And you want to query both from the USER namespace. In that case create a new class extending Sample.Person in the USER namespace and modify storage like this:

Class Utils.SamplesPerson Extends (%Persistent, Sample.Person)
{

Storage Default
{
<Data name="PersonDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^["SAMPLES"]Sample.PersonD</DataLocation>
<DefaultData>PersonDefaultData</DefaultData>
<ExtentSize>200</ExtentSize>
<IdLocation>^["SAMPLES"]Sample.PersonD</IdLocation>
<IndexLocation>^["SAMPLES"]Sample.PersonI</IndexLocation>
<StreamLocation>^["SAMPLES"]Sample.PersonS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

Note that DataLocation, IndexLocation, IdLocation and StreamLocation point to the SAMPLES namespace.

Now query:

SELECT *
FROM Utils.SamplePerson

Would fetch data from SAMPLES namespace.