go to post Timothy Leavitt · Sep 4, 2019 I don't think it's totally stupid. We're looking at doing something similar with another technology (JReport).Here's a code sample to help you get started - just change the port / page / filters / permission checks appropriately. Class Demo.CSPProxy Extends %CSP.Page { Parameter HOST = "127.0.0.1"; Parameter PORT = 8888; Parameter PAGE = "jinfonet/runReport.jsp"; /// Event handler for <b>PreHTTP</b> event: this is invoked before /// the HTTP headers for a CSP page have been sent. All changes to the /// <class>%CSP.Response</class> class, such as adding cookies, HTTP headers, /// setting the content type etc. must be made from within the OnPreHTTP() method. /// Also changes to the state of the CSP application such as changing /// %session.EndSession or %session.AppTimeout must be made within the OnPreHTTP() method. /// It is prefered that changes to %session.Preserve are also made in the OnPreHTTP() method /// as this is more efficient, although it is supported in any section of the page. /// Return <b>0</b> to prevent <method>OnPage</method> from being called. ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ] { Set request = ##class(%Net.HttpRequest).%New() Set request.Server = ..#HOST Set request.Port = ..#PORT // TODO: Add other stuff here, like authentication. Set page = $Piece(%request.CgiEnvs("REQUEST_URI"),"/"_$classname()_".cls/",2) If (page = "") { Set %base = $classname()_".cls/"_$Piece(..#PAGE,"/",1,*-1)_"/" // TODO: add query parameters from %request to the URL requested below. $$$ThrowOnError(request.Get(..#PAGE)) } Else { Set fullPage = "http://"_..#HOST_":"_..#PORT_"/"_page Do ##class(%Net.URLParser).Parse(fullPage,.parts) // TODO: Better way of checking the requested resource. If $Piece($Piece(parts("path"),"/",*),".",2) = "jsp" { Set %response.Status = ##class(%CSP.REST).#HTTP403FORBIDDEN Quit 0 } $$$ThrowOnError(request.Send(%request.Method,page)) } Set %data = request.HttpResponse.Data // TODO: Do any other headers matter? Set %response.Status = request.HttpResponse.StatusCode Set %response.ContentType = request.HttpResponse.ContentType Quit 1 } /// Event handler for <b>PAGE</b> event: this is invoked in order to /// generate the content of a csp page. ClassMethod OnPage() As %Status [ ServerOnly = 1 ] { If (%response.ContentType [ "html") && $Data(%base) { &html<<base href="#(..EscapeHTML(%base))#">> Do %data.OutputToDevice() } Else { Do %data.OutputToDevice() } Quit $$$OK } }
go to post Timothy Leavitt · Apr 1, 2019 A few weird things with custom login pages:CSPSystem needs read permission on the DB containing them. (You'll need to close CSPGateway connections after making this change so that it can reconnect with the right privileges.)It's specified with the ".cls" extension in the web application configuration, not just the classname.
go to post Timothy Leavitt · Jan 15, 2019 You should check the SQLCODE from running that statement. If SQLCODE is 100 (no record exists with the specified key), KeyID may not be defined.See: https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_esql#GSQL_esql_hvars
go to post Timothy Leavitt · Jan 3, 2019 "Why?" (a good question) and "Don't do that!" aside, here's a technical answer: override %OnDetermineClass to allow instances of the parent class to be treated as the subclass in question. For example: Class DC.Demo.Extension.BaseRecord Extends %Persistent { Property Name As %String; ClassMethod %OnDetermineClass(oid As %ObjectIdentity, ByRef class As %String) As %Status [ ServerOnly = 1 ] { Set tSC = ##super(oid,.class) If (class = "DC.Demo.Extension.BaseRecord") { Set class = $classname() } Quit tSC } Storage Default { <Data name="BaseRecordDefaultData"> <Value name="1"> <Value>%%CLASSNAME</Value> </Value> <Value name="2"> <Value>Name</Value> </Value> </Data> <DataLocation>^DC.Demo.Extension.BaseRecordD</DataLocation> <DefaultData>BaseRecordDefaultData</DefaultData> <IdLocation>^DC.Demo.Extension.BaseRecordD</IdLocation> <IndexLocation>^DC.Demo.Extension.BaseRecordI</IndexLocation> <StreamLocation>^DC.Demo.Extension.BaseRecordS</StreamLocation> <Type>%Library.CacheStorage</Type> } } Class DC.Demo.Extension.SubRecord Extends BaseRecord { Property Foo As %String; Storage Default { <Data name="SubRecordDefaultData"> <Subscript>"SubRecord"</Subscript> <Value name="1"> <Value>Foo</Value> </Value> </Data> <DefaultData>SubRecordDefaultData</DefaultData> <Type>%Library.CacheStorage</Type> } } Class DC.Demo.Extension.Driver { ClassMethod Run() { Do ##class(DC.Demo.Extension.BaseRecord).%KillExtent() Set tBaseRecord = ##class(DC.Demo.Extension.BaseRecord).%New() Set tBaseRecord.Name = "Fred" Write !,"Save base record: ",tBaseRecord.%Save() Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",! zw ^DC.Demo.Extension.BaseRecordD Set tSubRecord = ##class(DC.Demo.Extension.SubRecord).%OpenId(1) Set tSubRecord.Foo = "Bar" Write !,"Open as sub record, name = ",tSubRecord.Name Write !,"Save sub record (converts to sub record): ",tSubRecord.%Save() Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",! zw ^DC.Demo.Extension.BaseRecordD } } Output is: USER>d ##class(DC.Demo.Extension.Driver).Run() Save base record: 1 Contents of global ^DC.Demo.Extension.BaseRecordD: ^DC.Demo.Extension.BaseRecordD=1 ^DC.Demo.Extension.BaseRecordD(1)=$lb("","Fred") Open as sub record, name = Fred Save sub record (converts to sub record): 1 Contents of global ^DC.Demo.Extension.BaseRecordD: ^DC.Demo.Extension.BaseRecordD=1 ^DC.Demo.Extension.BaseRecordD(1)=$lb("~DC.Demo.Extension.SubRecord~","Fred") ^DC.Demo.Extension.BaseRecordD(1,"SubRecord")=$lb("Bar")
go to post Timothy Leavitt · Dec 6, 2018 On IRIS, you can call irisdb.exe the same way you used to call cache.exe.Compare:https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls... (IRIS)https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY... (Caché/Ensemble)
go to post Timothy Leavitt · Nov 16, 2018 The %System/%System/RoutineChange audit event is the only thing other than a source control hook that comes to mind. That's handy for detecting source code changes after the fact (when a class is compiled), but not intercepting attempts to change source code via an editor. What's your specific use case?
go to post Timothy Leavitt · Jul 12, 2018 After further review, I'm really not sure why the first/second queries don't use the index. The problem with the last query is that %Key is the index in the list, not anything about Tag itself.Here's a solution that performs well in my testing: Class DC.Demo.Tag Extends (%Persistent, %Populate) { Index Tag On Tag [ Unique ]; Property Tag As %String; } Class DC.Demo.Tagged Extends (%Persistent, %Populate) { Relationship HasTags As DC.Demo.HasTag [ Cardinality = children, Inverse = Tagged ]; ClassMethod Run() { Do ..%KillExtent() Do ##class(DC.Demo.Tag).%KillExtent() Do ##class(DC.Demo.Tag).Populate(50) Do ..Populate(5000) Do ##class(DC.Demo.HasTag).Populate(10000) } } Class DC.Demo.HasTag Extends (%Persistent, %Populate) { Relationship Tagged As DC.Demo.Tagged [ Cardinality = parent, Inverse = HasTags ]; Property Tag As DC.Demo.Tag [ Required ]; Index UniqueTag On Tag [ IdKey ]; Index TaggedByTag On (Tag, Tagged); }
go to post Timothy Leavitt · Jun 13, 2018 It's not the prettiest, but I think the simplest solution would be to avoid navigating to the parent object entirely:Add a Foobar property to EmbedObj.Via a row/object trigger in ContainerObj, propagate changes to Foobar to EmbedObj_Foobar.As an initial step for data population in existing records, run SQL: update ContainerObj set EmbedObj_Foobar = FoobarBase your SQLComputeCode on the copy of Foobar in the serial class.
go to post Timothy Leavitt · Jun 12, 2018 Re: extending method keywords, you can't do that at this time, but a useful approximation is structuring a comment - for example: /// @MyKeyword MyValue ClassMethod TestOne() { // Implementation } And then looking at the %Dictionary.MethodDefinition / %Dictionary.CompiledMethod documentation in a generator method. (But it looks like you might already be on to that with @AutoGenerated.) Re: making first compilation work, this works for me, by making the first compilation automatically trigger a second one when needed: ClassMethod GenerateMethods() As %Status [ CodeMode = objectgenerator ] { For i=1:1:%class.Methods.Count() { #dim method As %Dictionary.MethodDefinition = %class.Methods.GetAt(i) Continue:((method.Description["@AutoGenerated") || (method.CodeMode="objectgenerator")) Do %class.Methods.Insert(##class(util.TestGenerator).Generate(method.Name_"DoSomethingElse",%class.Name)) } If (%class.Methods.%IsModified()) { Do ##class(%Projection.AbstractProjection).QueueClass(%class.Name) } Quit %class.%Save() } Test code: Class util.Driver { ClassMethod Run() { do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestOneDoSomethingElse") do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestTwoDoSomethingElse") do $system.OBJ.UnCompile("util.*") do $system.OBJ.Compile("util.*","ck") } }
go to post Timothy Leavitt · Jun 8, 2018 If you want to count (or otherwise traverse) all the elements in a multidimensional array, you can use $Query - here's a sample with method that does that: ClassMethod Run() { Set a(1) = "blue" Set a(1,3,5,2) = "navy" Set a(2) = "red" Set a(2,2) = "crimson" Set a(3) = "yellow" Write ..Count(.a) } ClassMethod Count(ByRef pArray) As %Integer [ PublicList = pArray ] { Set count = 0 Set ref = $Query(pArray("")) While (ref '= "") { Set count = count + 1 Set ref = $Query(@ref) } Quit count }
go to post Timothy Leavitt · May 30, 2018 The key point here is how to get the value of the "DE" property parameter from the OPTREGISTER class definition into the "DE" property of the corresponding property. The naming does not do that automatically. Generally, property parameters (like DE, DICLookUp, XMLNAME) are used for code generation, either in methods (the third example below), projections (like XML), or for validation of datatypes (e.g., %String MINLEN/MAXLEN).Assuming XML export is done with (for example): Set o = ##class(Test.SendExternalModel.OPTREGISTER).%New() Do o.XMLExport(,",literal,indent") There are at least three options. I'd recommend the third, but the first two are perhaps easier to understand. 1) In a method of OPTREGISTER (perhaps %OnNew), instantiate the various properties and set the DE property of each of them. In this case, the DE property parameter is no longer needed. For example: Method %OnNew() As %Status [ Private, ServerOnly = 1 ] { Set ..THBZ = ##class(Test.SendExternalModel.DE2).%New() Set ..THBZ.DE = "DEX71.41.007.01" Set ..GHFS = ##class(Test.SendExternalModel.DE2).%New() Set ..GHFS.DE = "DEX71.41.008.01" Set ..GHF = ##class(Test.SendExternalModel.DE1).%New() Set ..GHF.DE = "DEX71.41.009.01" Set ..ZLF = ##class(Test.SendExternalModel.DE1).%New() Set ..ZLF.DE = "DEX71.41.009.01" Set ..QTF = ##class(Test.SendExternalModel.DE1).%New() Set ..QTF.DE = "DEX71.41.009.01" Set ..WDBZ = ##class(Test.SendExternalModel.DE2).%New() Set ..WDBZ.DE = "DEX71.41.010.01" Set ..GHKSDM = ##class(Test.SendExternalModel.DE2).%New() Set ..GHKSDM.DE = "DE08.10.025.00" Quit $$$OK } 2) Have DE1/DE2 extend %SerialObject rather than %RegisteredObject, and define a constructor in DE1. (DE2 can have inheritance simplified a bit, although perhaps not in the context of your full application.) Then, define properties in OPTREGISTER with an InitialExpression - this is passed to the constructor for the serial object. This approach may not actually make sense in the context of your full application. PropertyParameters: Class Test.Common.PropertyParameters Extends %XML.PropertyParameters [ ProcedureBlock ] { Parameter HDSD As STRING; Parameter DE As STRING; Parameter DICLookUp As STRING; } DE1: Class Test.SendExternalModel.DE1 Extends (%SerialObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ] { Parameter XMLIGNOREINVALIDTAG = 1; Parameter XMLIGNORENULL = 1; Property DE As %String(XMLNAME = "de", XMLPROJECTION = "ATTRIBUTE"); Method %OnNew(pDE As %String = "") As %Status [ Private, ServerOnly = 1 ] { Set ..DE = pDE Quit $$$OK } Storage Default { <Data name="DE1State"> <Value name="1"> <Value>DE</Value> </Value> </Data> <State>DE1State</State> <StreamLocation>^Test.SendExternalModel.DE1S</StreamLocation> <Type>%Library.CacheSerialState</Type> } } DE2: Class Test.SendExternalModel.DE2 Extends Test.SendExternalModel.DE1 { Property Display As %String(XMLNAME = "display", XMLPROJECTION = "ATTRIBUTE"); Storage Default { <Data name="DE2State"> <Value name="1"> <Value>DE</Value> </Value> <Value name="2"> <Value>Display</Value> </Value> </Data> <State>DE2State</State> <StreamLocation>^Test.SendExternalModel.DE2S</StreamLocation> <Type>%Library.CacheSerialState</Type> } } OPTREGISTER: Class Test.SendExternalModel.OPTREGISTER Extends (%RegisteredObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ] { Parameter XMLIGNOREINVALIDTAG = 1; Parameter XMLIGNORENULL = 1; Parameter XMLNAME = "OPTREGISTER"; Property THBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.007.01", DICLookUp = "Y", XMLNAME = "THBZ") [ InitialExpression = "DEX71.41.007.01", Required ]; Property GHFS As Test.SendExternalModel.DE2(DE = "DEX71.41.008.01", DICLookUp = "Y", XMLNAME = "GHFS") [ InitialExpression = "DEX71.41.008.01", Required ]; Property GHF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "GHF") [ InitialExpression = "DEX71.41.009.01", Required ]; Property ZLF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "ZLF") [ InitialExpression = "DEX71.41.009.01", Required ]; Property QTF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "QTF") [ InitialExpression = "DEX71.41.009.01", Required ]; Property WDBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.010.01", DICLookUp = "Y", XMLNAME = "WDBZ") [ InitialExpression = "DEX71.41.010.01" ]; Property GHKSDM As Test.SendExternalModel.DE2(DE = "DE08.10.025.00", DICLookUp = "Y", XMLNAME = "GHKSDM") [ InitialExpression = "DE08.10.025.00", Required ]; } 3) Given all of your original class definitions, add a generator method in OPTREGISTER (or a parent class) that sets the "DE" property of any properties of type DE1/DE2 (or perhaps any properties that have your custom property parameters class) based on the DE property parameter in the containing class's definition. Ensure this method is called prior to XML export. (Might be useful if you need more fine-grained control - i.e., omitting the WDBZ element if it has no content.) This would be a great approach if you have lots of similar classes like these, to avoid repeating (1) every time. A really simple implementation (without any extra sanity checking for property types, etc.), equivalent to the first example, would look like: Method %OnNew() As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ] { Set tKey = "" For { #dim tProperty As %Dictionary.CompiledProperty Set tProperty = %compiledclass.Properties.GetNext(.tKey) If (tKey = "") { Quit } Set tDE = tProperty.Parameters.GetAt("DE") If (tDE '= "") { Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_" = ##class("_tProperty.Type_").%New()") Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_".DE = "_$$$QUOTE(tDE)) } } Do %code.WriteLine(" Quit $$$OK") Quit $$$OK }
go to post Timothy Leavitt · May 30, 2018 One option is to increase $zstorage (see documentation) to let the process use more memory; since it's just a one-time import, this may be simplest. Another option is to set the UsePPGHandler property to true (1) on the instance of %XML.Reader - this has a negative impact on performance.
go to post Timothy Leavitt · May 18, 2018 In the possible absence of a built-in class for such a purpose, this seems to work: Class DC.Demo.GlobalReference Extends %String [ ClassType = datatype ] { /// 511 is an upper bound for the maximum length of a global reference - see: /// <a href="https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GGBL_structure#GGBL_structure_maxsubscrlen">Maximum Length of a Global Reference</a> Parameter MAXLEN = 511; ClassMethod IsValid(%val As %CacheString) As %Status [ ServerOnly = 0 ] { Set tOldZReference = $ZReference Set tSC = $$$OK Try { Set $ZReference = %val } Catch e { // The above SET will throw a <SYNTAX> exception for an invalid global reference Set tSC = $$$ERROR($$$GeneralError,$$$FormatText("Invalid global reference: %1",%val)) } Set $ZReference = tOldZReference Quit tSC } }
go to post Timothy Leavitt · Apr 30, 2018 This can be done with the requiredMessage property of %ZEN.Component.control. There are two ways to accomplish this: 1. Just add the requiredMessage attribute Class DC.Demo.ZenLocalization Extends %ZEN.Component.page { XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] { <page xmlns="http://www.intersystems.com/zen"> <form> <text label="Descrição" required="true" requiredMessage="obrigatório." /> <submit caption="Enviar" /> </form> </page> } } Problem is, you'd need to do that in a lot of different places. Instead, you could... 2. Use custom components that subclass built-in control types. Sample component: Class DC.Demo.Component.text Extends %ZEN.Component.text [ System = 3 ] { /// Feel free to customize this. Parameter NAMESPACE = "https://community.intersystems.com/post/change-language-required"; /// Value displayed in alert box by the form <method>validate</method> /// method when this control is required and does not have a value.<br> /// This is a localized value. Property requiredMessage As %ZEN.Datatype.caption [ InitialExpression = "obrigatório." ]; } Sample page using component: Class DC.Demo.ZenLocalization Extends %ZEN.Component.page { XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ] { <page xmlns="http://www.intersystems.com/zen" xmlns:custom="https://community.intersystems.com/post/change-language-required"> <form> <custom:text label="Descrição" required="true" /> <submit caption="Enviar" /> </form> </page> } } I was hoping there would be a way to use Zen's localization features to do this, but it seems like that isn't an option, unfortunately;"required." is hard-coded as the InitialExpression for requiredMessage in %ZEN.Component.control, and may only be localized within a page using the component if a non-default value is specified.
go to post Timothy Leavitt · Apr 27, 2018 There was an issue with database compaction in 2014.1.1 that could cause database degradation - see this alert: https://www.intersystems.com/support-learning/support/product-news-alerts/support-alert/alert-database-compaction/Given that, I would recommend contacting the experts in the Worldwide Response Center (InterSystems Support) to help investigate. Running an integrity check on the database you compacted would be a good start - see https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_integrity#GCDI_integrity_verify_portal.
go to post Timothy Leavitt · Mar 29, 2018 Another slightly more lightweight approach: Class DC.Demo.combobox Extends %ZEN.Component.combobox [ System = 3 ] { Parameter NAMESPACE = "http://community.intersystems.com/demo"; /// Notification that this component is about to become modal. ClientMethod onStartModalHandler(zindex) [ Language = javascript ] { this.invokeSuper('onStartModalHandler',arguments); this.fixLinks(this.getDropDownDiv()); } /// Look for all children of specified element, and change links with href="#" to also have onclick = "return false;" ClientMethod fixLinks(element) [ Language = javascript ] { for (var i = 0; i < element.children.length; i++) { this.fixLinks(element.children[i]); } if (element.getAttribute("href") == "#") { element.onclick = function() { return false; }; } } }
go to post Timothy Leavitt · Mar 28, 2018 We specifically chose not to support this capability in Atelier, other than for the special case of launching web pages. As an implementation of Studio Source Control, running an executable on the client only makes sense in cases where the "server" and the "client" are always the same, and for that model the source control plugins publicly and freely available in the Eclipse ecosystem are far superior in terms of support and usability.I would be interested to hear more details of your use case. What version control system are you using? Git?(Note to other readers: "UserAction = 3" and other details of the Studio Extension framework are documented here.)
go to post Timothy Leavitt · Mar 20, 2018 What are you trying to do? This? USER>Set rm=##class(%Regex.Matcher).%New("[0-9]","Word 123456") USER>Write rm.ReplaceAll(".") Word ...... Or: USER>Set rm=##class(%Regex.Matcher).%New("[0-9]+","Word 123456") USER>Write rm.ReplaceAll(".") Word .
go to post Timothy Leavitt · Feb 6, 2018 USER>d $System.OBJ.GetPackageList(.classes,"%CSP.Documatic") zw classes classes("%CSP.Documatic.CubeInfo")="" classes("%CSP.Documatic.PrintClass")=""
go to post Timothy Leavitt · Nov 28, 2017 You could have your unit test class extend the class with the [Private] method, or (if that doesn't make sense or causes issues) write another class that your unit test loads that extends the class with the [Private] method and has another method that wraps it.