# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: marian.edu@acorn.ro-20210131101452-ui20dacrs58vsdlj # target_branch: sftp://medu@xfer.goldencode.com/opt/fwd/4384g/ # testament_sha1: f5f7eca50742231137f87677da03f7f60bdd4f38 # timestamp: 2021-02-01 10:09:59 +0200 # base_revision_id: marian.edu@acorn.ro-20210130114536-\ # y9e0nzy27pjk8fut # # Begin patch === modified file 'build.gradle' --- build.gradle 2020-09-15 14:26:57 +0000 +++ build.gradle 2020-11-16 21:38:16 +0000 @@ -64,6 +64,11 @@ ** 037 HC 20200726 Initial implementation of SPREADSHEET widget and related changes. ** CA 20200914 Added aspectjtools to fwdConvert group. ** CA 20200915 Upgraded to Jetty 9.4.22. +** CA 20201011 Added RoaringBitmap, a fast access bitmap for integer or long values, which can be used +** instead of hash sets. +** ECF 20201116 Changed version of fwd-h2 to 1.4.200-5. Note the naming convention change, by which the +** last component of the name is now the fwd-h2 revision number, rather than a sortable +** date. */ /* @@ -307,7 +312,7 @@ fwdConvertServer group: 'org.jboss.logging', name: 'jboss-logging', version: '3.1.0.GA' fwdConvertServer group: 'antlr', name: 'antlr', version: '2.7.7' - fwdConvertServer group: 'com.goldencode', name: 'fwd-h2', version: '1.4.200-20200727' + fwdConvertServer group: 'com.goldencode', name: 'fwd-h2', version: '1.4.200-5' fwdConvertServer group: 'javax.transaction', name: 'jta', version: '1.1' fwdConvertClient (group: 'com.google.guava', name: 'guava', version: '19.0') { @@ -353,6 +358,7 @@ fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-client', version: '0.10' fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-utils', version: '0.10' fwdClientServer group: 'com.esotericsoftware', name: 'reflectasm', version: '1.11.3' + fwdClientServer group: 'org.roaringbitmap', name: 'RoaringBitmap', version: '0.9.0' fwdServer group: 'org.apache.tuscany.sdo', name: 'tuscany-sdo-impl', version: '1.1.1' fwdServer group: 'org.apache.tuscany.sdo', name: 'tuscany-sdo-tools', version: '1.1.1' === modified file 'build.xml' --- build.xml 2020-09-07 16:23:31 +0000 +++ build.xml 2021-01-05 13:24:19 +0000 @@ -195,7 +195,9 @@ ** 101 VVT 20200203 Added cursor UI resources (.cur files) to jar location. ** 102 OM 20200108 DMO Impl generation TRPL dropped from runtime support. ** 103 HC 20200726 Initial implementation of SPREADSHEET widget and related changes. -*/ +** HC 20201020 Removed ext sheet sources from javadoc target. Javadoc is handled by the sheet +** project itself. +** --> @@ -485,13 +486,62 @@ + + + + + copy.type == prog.assign or + copy.type == prog.equals or + copy.type == prog.lt or + copy.type == prog.lte or + copy.type == prog.gt or + copy.type == prog.gte + + + + child = copy.firstChild + ch1type = ecw.getCompatibilityClass(child) + + child = child.nextSibling + ch1type != null and + !ch1type.equals("unknown") and + !ch1type.equals("basedatatype") and + !child.isAnnotation("read-only attribute helper") + + ch2type = ecw.getCompatibilityClass(child) + + !ch1type.equals(ch2type) + !isRuntimeConfig() + + + printfln("WARNING: Incompatible data types in expression or assignment. The generated code may not compile or may work incorrectly.") + + + + printfln("Additional info: ch1type=%s ch2type=%s\n%s", ch1type, ch2type, copy.dumpTree(true)) + + isRuntimeConfig() + + compileError(223, "Incompatible data types in expression or assignment") + + + + + + + - + - + === modified file 'rules/annotations/database_general.rules' --- rules/annotations/database_general.rules 2020-09-10 09:54:53 +0000 +++ rules/annotations/database_general.rules 2021-01-21 22:49:45 +0000 @@ -5,9 +5,9 @@ ** Module : database_general.rules ** Abstract : database related annotations ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ __JPRM__ ____________________________Description_____________________________ +** _#_ _I_ __Date__ __JPRM__ ___________________________________Description___________________________________ ** 001 ECF 20051207 @23763 Created initial version. Miscellaneous rules ** for annotating various database language ** statements and functions. @@ -91,10 +91,8 @@ ** connect options. ** 035 ECF 20170820 Added support for read-xml and write-xml methods. ** 036 OM 20170823 Fixed DISABLE TRIGGERS statement. -** 037 GES 20170827 CONNECT_TEXT nodes can have quoted text in which the quotes were -** being doubled. -** 038 ECF 20171025 Added -ct (max connection retries) to supported CONNECT statement -** options. +** 037 GES 20170827 CONNECT_TEXT nodes can have quoted text in which the quotes were being doubled. +** 038 ECF 20171025 Added -ct (max connection retries) to supported CONNECT statement options. ** 039 OM 20171215 Force fields to have alias only if the quick-delete has subqueries. ** 040 ECF 20180118 API name change in SchemaWorker. ** 041 OM 20180309 The id-related functions are converted earlier into corresponding @@ -108,8 +106,7 @@ ** currently is not implemented. ** 046 ECF 20181208 Added support for read-json and write-json methods. ** 047 CA 20181221 Fixed FIND-CURRENT lock literals. -** 048 ECF 20190214 Rearrange BUF_{COPY|COMP} statement child nodes for easier downstream -** conversion. +** 048 ECF 20190214 Rearrange BUF_{COPY|COMP} statement child nodes for easier downstream conversion. ** CA 20190323 FOR EACH ... DELETE. blocks conversion to EMPTY TEMP-TABLE must be ** done only after buffer scopes are computed. ** 049 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source. @@ -121,6 +118,7 @@ ** 054 IAS 20200212 Added support for new CONNECT options. ** 055 ECF 20200906 New ORM implementation. ** 056 CA 20200910 Added BUFFER:SERIALIZE-ROW() method conversion. +** OM 20210120 Kept resource type parameter in order to validate it at runtime. --> parent.type == prog.meth_logical - oldtype = parent.getAnnotation("oldtype") oldtype == prog.kw_read_xsc or @@ -922,36 +919,9 @@ pos1 = this.indexPos - - (pos1 == 0 and oldtype != prog.kw_serialzr) or (pos1 == 1 and oldtype == prog.kw_serialzr) - - - oldtype == prog.kw_read_xml or - oldtype == prog.kw_read_jsn or - oldtype == prog.kw_read_xsc - copy.setHidden(true) - - - - oldtype == prog.kw_wr_xml or - oldtype == prog.kw_wr_json or - oldtype == prog.kw_wr_xmlsc or - oldtype == prog.kw_serialzr - - javaname = ecw.expressionType(#(com.goldencode.ast.Aast) this.nextSibling) - - javaname == "memptr" or javaname == "longchar" - copy.setHidden(true) - - - - - - + (pos1 >= 2 and oldtype != prog.kw_serialzr) or ((pos1 == 0 or pos1 >= 3) and oldtype == prog.kw_serialzr) putNote("use-method-peerid", true) === modified file 'rules/annotations/legacy_services.rules' --- rules/annotations/legacy_services.rules 2020-10-08 20:26:22 +0000 +++ rules/annotations/legacy_services.rules 2021-01-13 21:04:41 +0000 @@ -12,7 +12,7 @@ ** 002 CA 20190703 Added support for WebHandler services. ** 003 CA 20200514 Added support for SOAP web services. ** 004 GES 20200723 Added address attribute for REST. -** 005 CA 20201008 Annotate any temp-table, dataset, parameter, internal procedure and program which must be +** CA 20201008 Annotate any temp-table, dataset, parameter, internal procedure and program which must be ** generated into the legacy open client proxy Java program. */ --> === modified file 'rules/annotations/ocx_conversion.rules' --- rules/annotations/ocx_conversion.rules 2020-09-07 16:23:31 +0000 +++ rules/annotations/ocx_conversion.rules 2021-01-13 21:04:41 +0000 @@ -51,9 +51,11 @@ ** 20200608 Added NODECLICK, BEFORELABELEDIT, AFTERLABELEDIT event triggers. ** 012 SBI 20200619 Changed NODECLICK, BEFORELABELEDIT, AFTERLABELEDIT to be according with FWD ** widget events naming convention, AFTERLABELEDIT can update the label's value. -** 013 SBI 20200722 Added mapping for child, previous, next, firstsibling, lastsibling. +** SBI 20200722 Added mapping for child, previous, next, firstsibling, lastsibling. ** HC 20200726 Improved OCX conversion of COM properties. ** HC 20200827 Fixed conversion of assignment of COM properties with multiple indexes. +** CA 20201109 Expose the CALENDAR:VALUE as a 'CalendarValue' attribute which follows the 4GL's datetime +** string representation, and not ISO-8601. */ --> map6 = create("java.util.HashMap") - map6.put("value" , "DateTimeValue") + map6.put("value" , "CalendarValue") map6.put("checkbox" , "Checked") map6.put("font" , "FontInfo") map6.put("format" , "FormatStyle") === modified file 'rules/annotations/record_scoping.rules' --- rules/annotations/record_scoping.rules 2019-05-30 23:38:09 +0000 +++ rules/annotations/record_scoping.rules 2020-10-30 14:02:14 +0000 @@ -5,9 +5,9 @@ ** Module : record_scoping.rules ** Abstract : calculates record scopes ** -** Copyright (c) 2005-2019, Golden Code Development Corporation. +** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ __JPRM__ ________________Description_________________ +** _#_ _I_ __Date__ __JPRM__ _________________________________Description__________________________________ ** 001 GES 20050919 @22811 Calculates record scopes in a Progress ** compatible manner, including support for ** all of the implicit scope expansion rules. @@ -61,6 +61,8 @@ ** 019 CA 20190128 Track buffer's static state (if OO). ** 020 CA 20190513 Fixed buffer usage from super-classes, in OO. ** 021 CA 20190530 Fixed staging of buffer parameters for OO methods or constructors. +** 022 ECF 20201029 Create a buffer scope for NEW SHARED defined buffers, in case the buffer is +** not used elsewhere in the procedure where it was defined. */ --> + type == prog.block and getNoteBoolean("recordScoping") buf.pushScope(copy) @@ -167,18 +168,18 @@ - + + - ((type == prog.define_buffer and this.getChildAt(0).type == prog.kw_shared) or - type == prog.define_temp_table or + ((type == prog.define_buffer and this.getImmediateChild(prog.kw_shared, null) != null) or + type == prog.define_temp_table or type == prog.define_work_table) and not ancestor(prog.interface_def, -1) - - type == prog.define_buffer and this.getChildAt(0).type == prog.kw_shared + + type == prog.define_buffer and this.getImmediateChild(prog.kw_shared, null) != null ref = copy.getImmediateChild(prog.kw_for, null) ref.downPath("TEMP_TABLE") ref = ref.getImmediateChild(prog.temp_table, null) === modified file 'rules/convert/builtin_functions.rules' --- rules/convert/builtin_functions.rules 2020-09-07 16:23:31 +0000 +++ rules/convert/builtin_functions.rules 2021-01-13 21:04:41 +0000 @@ -7,7 +7,7 @@ ** ** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ __JPRM__ ____________________________Description_____________________________ +** _#_ _I_ __Date__ __JPRM__ ___________________________________Description___________________________________ ** 001 GES 20050721 @21759 Good working version which supports a wide ** range of the most common builtin functions ** including all type conversion, math, string @@ -176,6 +176,8 @@ ** GES 20200506 Changed the name of DYNAMIC-ENUM(). ** SBI 20200529 Implemented LOAD-PICTURE. ** 102 CA 20200811 SUPER() function has as first argument the caller's return type. +** EVL 20201216 Adding function to interrupt metafile recording. +** OM 20201120 Added DATA-SOURCE-MODIFIED function. Changed converted ERROR signature. */ --> @@ -265,15 +267,15 @@ ftype == prog.kw_mf_gfsd or ftype == prog.kw_mf_gfsr or ftype == prog.kw_mf_gph or ftype == prog.kw_mf_gpn or ftype == prog.kw_mf_gpw or ftype == prog.kw_mf_gtw or ftype == prog.kw_mf_gxy or ftype == prog.kw_mf_gzf or ftype == prog.kw_mf_init or - ftype == prog.kw_mf_mpdf or ftype == prog.kw_mf_p2mu or ftype == prog.kw_mf_rp or - ftype == prog.kw_mf_sf or ftype == prog.kw_mf_sfc or ftype == prog.kw_mf_sfh or - ftype == prog.kw_mf_si or ftype == prog.kw_mf_sia or ftype == prog.kw_mf_sla or - ftype == prog.kw_mf_slc or ftype == prog.kw_mf_slm or ftype == prog.kw_mf_sls or - ftype == prog.kw_mf_snp or ftype == prog.kw_mf_sntl or ftype == prog.kw_mf_spf or - ftype == prog.kw_mf_spn or ftype == prog.kw_mf_spnp or ftype == prog.kw_mf_spnt or - ftype == prog.kw_mf_spo or ftype == prog.kw_mf_sr or ftype == prog.kw_mf_sta or - ftype == prog.kw_mf_stc or ftype == prog.kw_mf_sts or ftype == prog.kw_mf_sxy or - ftype == prog.kw_mf_szf + ftype == prog.kw_mf_ir or ftype == prog.kw_mf_mpdf or ftype == prog.kw_mf_p2mu or + ftype == prog.kw_mf_rp or ftype == prog.kw_mf_sf or ftype == prog.kw_mf_sfc or + ftype == prog.kw_mf_sfh or ftype == prog.kw_mf_si or ftype == prog.kw_mf_sia or + ftype == prog.kw_mf_sla or ftype == prog.kw_mf_slc or ftype == prog.kw_mf_slm or + ftype == prog.kw_mf_sls or ftype == prog.kw_mf_snp or ftype == prog.kw_mf_sntl or + ftype == prog.kw_mf_spf or ftype == prog.kw_mf_spn or ftype == prog.kw_mf_spnp or + ftype == prog.kw_mf_spnt or ftype == prog.kw_mf_spo or ftype == prog.kw_mf_sr or + ftype == prog.kw_mf_sta or ftype == prog.kw_mf_stc or ftype == prog.kw_mf_sts or + ftype == prog.kw_mf_sxy or ftype == prog.kw_mf_szf bRet = true @@ -360,6 +362,7 @@ fwdId2Mf.put(prog.kw_mf_gxy , "getXY") fwdId2Mf.put(prog.kw_mf_gzf , "getZoomFactor") fwdId2Mf.put(prog.kw_mf_init , "initialize") + fwdId2Mf.put(prog.kw_mf_ir , "interruptRecording") fwdId2Mf.put(prog.kw_mf_mpdf , "makePdf") fwdId2Mf.put(prog.kw_mf_p2mu , "pix2MfUnits") @@ -552,12 +555,18 @@ dbimport = true + + ftype == prog.kw_data_sm + methodText = "DataSourceModifiable.isDataSourceModified" + dbimport = true + + ftype == prog.kw_date methodText = "date" methodType = java.constructor - + ftype == prog.kw_datetime methodText = "datetime" methodType = java.constructor @@ -567,7 +576,7 @@ methodText = "datetimetz" methodType = java.constructor - + ftype == prog.kw_day methodText = "date.day" @@ -756,14 +765,13 @@ extent = deref.getAnnotation("oldextent") - + extent == null or extent == 0 methodText = "0" methodType = java.num_literal - + (extent != null and extent == -1) or (copy.parent.isAnnotation("wrap") and @@ -1341,13 +1349,6 @@ dbimport = true - ftype == prog.kw_error - methodType = java.method_call - methodText = "isError" - - dbimport = true - - ftype == prog.kw_rejected methodType = java.method_call methodText = "rejected" @@ -1655,8 +1656,7 @@ raiseError - errmsg = sprintf("Unsupported builtin function %s.", - this.lookupTokenName(ftype)) + errmsg = sprintf("Unsupported builtin function %s.", this.lookupTokenName(ftype)) printfln("%s\n%s", errmsg, this.dumpTree(true)) === modified file 'rules/convert/legacy_services.rules' --- rules/convert/legacy_services.rules 2020-10-08 20:26:22 +0000 +++ rules/convert/legacy_services.rules 2021-01-08 17:01:46 +0000 @@ -5,10 +5,11 @@ ** Module : legacy_services.rules ** Abstract : conversion related to legacy proxy client code. ** -** Copyright (c) 2020, Golden Code Development Corporation. +** Copyright (c) 2020-2021, Golden Code Development Corporation. ** ** _#_ _I_ __Date__ _______________________________________Description________________________________________ ** 001 CA 20201007 Created initial version. +** CA 20210108 Fixed a typo when generating the Java proxy code for a temp-table field definition. */ --> @@ -366,7 +367,7 @@ createJavaAst(java.string, #(java.lang.String) propAst.getAnnotation("historical"), ref) - isNote("extent") + propAst.isAnnotation("extent") createJavaAst(java.num_literal, sprintf("%s", propAst.getAnnotation("extent")), ref) === modified file 'rules/convert/menu_generator.xml' --- rules/convert/menu_generator.xml 2020-03-23 11:35:33 +0000 +++ rules/convert/menu_generator.xml 2020-10-24 00:11:03 +0000 @@ -7,7 +7,7 @@ ** ** Copyright (c) 2015-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ ________________________________Description_________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 VIG 20141101 Initial version. ** 002 VIG 20150204 Added SUB-MENU label support, removeQuotes are replaced by ** ecw.progressToJavaString() method call. @@ -18,13 +18,14 @@ ** 005 OM 20150918 Fixed historical/java names. ** 006 VIG 20160621 Added labels collection to uiStrings file. ** 007 GES 20171017 Added hexadecimal literal support. -** 008 CA 20180308 Fixed classname conversion - it was using an aready converted javaname +** 008 CA 20180308 Fixed classname conversion - it was using an already converted javaname ** instead of the legacy name, so it was not converting properly. ** 009 GES 20180910 Resolved ambiguous method call (it did not fail previously but the correct ** method was not being called). ** 010 CA 20181208 Use progressToJavaString to emit the TITLE option. ** 011 CA 20200412 Added incremental conversion support. ** 012 SBI 20200322 Added label not null rule for processing menu items. +** 013 OM 20201016 Added NPE guard for uninitialized uiStrings. */ --> @@ -462,7 +463,9 @@ - execLib("collect_ui_strings", uiStrings) + uiStrings != null + execLib("collect_ui_strings", uiStrings) + === modified file 'rules/convert/method_definitions.rules' --- rules/convert/method_definitions.rules 2020-09-10 09:54:53 +0000 +++ rules/convert/method_definitions.rules 2021-01-13 21:04:41 +0000 @@ -7,7 +7,7 @@ ** ** Copyright (c) 2018-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ _______________________________Description_________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 GES 20181213 First version. ** 002 OM 20181218 Added generics for object getters. ** 003 GES 20190118 Changed "access" annotation to "access-mode" for methods. This is consistent @@ -29,7 +29,7 @@ ** CA 20200514 Emit the LegacySignature at the external program's 'execute' method and all its internal ** entries. ** 009 CA 20200804 Emit bulk setter and getter for an extent property. -** 010 CA 20200910 Fixed LegacyParameter annotation problem when DEFINE PARAMETER appears somewhere in a +** CA 20200910 Fixed LegacyParameter annotation problem when DEFINE PARAMETER appears somewhere in a ** nested block. */ --> === modified file 'rules/convert/methods_attributes.rules' --- rules/convert/methods_attributes.rules 2020-09-10 09:54:53 +0000 +++ rules/convert/methods_attributes.rules 2021-01-13 21:04:41 +0000 @@ -495,10 +495,16 @@ ** CA 20200503 Added ERROR:ERROR-STRING, QUERY:CACHE and BUFFER:IS-MULTI-TENANT. ** SVL 20200509 Added ALIGNMENT attribute. ** 222 SBI 20200619 Added NEW-LABEL attribute for the tree widget. -** 223 GES 20200627 Renamed Coordinates interface to be more generic. Added the undocumented +** GES 20200627 Renamed Coordinates interface to be more generic. Added the undocumented ** LAST-EVENT:SET-LASTKEY() method. Added the LAST-EVENT:COLUMN/ROW attributes ** support. Added the BUFFER-FIELD:DEFAULT-VALUE attribute. -** 224 CA 20200910 Added BUFFER:SERIALIZE-ROW() method conversion. +** CA 20200910 Added BUFFER:SERIALIZE-ROW() method conversion. +** CA 20201109 Expose the CALENDAR:VALUE as a 'CalendarValue' attribute which follows the 4GL's +** datetime string representation, and not ISO-8601. +** SVL 20201225 MIN-HEIGHT-CHARS is unwrapped using MinHeightCharsInterface. MAX-HEIGHT-CHARS is +** applicable only to Window widget +** OM 20201120 Conversion changes for multiple methods/attributes related to datasets and xml +** serialization. */ --> @@ -613,6 +619,7 @@ list.put(prog.kw_aud_ev_c, execLib("cr_descr", "ClientPrincipal" , "getAuditEventContext" , "setAuditEventContext" , true )) list.put(prog.kw_authen_f, execLib("cr_descr", "ClientPrincipal" , "authenticationFailed" , null , true )) list.put(prog.kw_auto_com, execLib("cr_descr", "Widget" , "isAutoCompletion" , "setAutoCompletion" , true )) + list.put(prog.kw_auto_del, execLib("cr_descr", "Buffer" , "autoDelete" , "autoDelete" , true )) list.put(prog.kw_auto_end, execLib("cr_descr", "Button" , "isAutoEndKey" , "setAutoEndKey" , true )) list.put(prog.kw_auto_go , execLib("cr_descr", "Button" , "isAutoGo" , "setAutoGo" , true )) list.put(prog.kw_auto_ind, execLib("cr_descr", "Editor" , "isAutoIndent" , "setAutoIndent" , true )) @@ -668,7 +675,7 @@ list.put(prog.kw_cur_qry , execLib("cr_descr", "DataRelation" , "getCurrentQuery" , null , true )) list.put(prog.kw_cvt_3d_c, execLib("cr_descr", "ImageSupport" , "getConvert3D" , "setConvert3D" , true )) list.put(prog.kw_data_scm, execLib("cr_descr", "Buffer" , "dataSourceCompleteMap" , null , true )) - list.put(prog.kw_data_sm , execLib("cr_descr", "DataSet" , "isDataSourceModified" , "setDataSourceModified" , true )) + list.put(prog.kw_data_sm , execLib("cr_descr", "DataSourceModifiable" , "isDataSourceModified" , "setDataSourceModified" , true )) list.put(prog.kw_data_src, execLib("cr_descr", "Buffer" , "dataSource" , null , true )) list.put(prog.kw_data_sri, execLib("cr_descr", "Buffer" , "dataSourceRowid" , "dataSourceRowid" , true )) list.put(prog.kw_dataset , execLib("cr_descr", "Buffer" , "dataSet" , null , true )) @@ -676,7 +683,7 @@ list.put(prog.kw_db_list , execLib("cr_descr", "ClientPrincipal" , "getDbList" , null , true )) list.put(prog.kw_deblank , execLib("cr_descr", "Deblank" , "isDeblank" , "setDeblank" , true )) list.put(prog.kw_debug , execLib("cr_descr", "Debugger" , "debug" , null , true )) - list.put(prog.kw_decimals, execLib("cr_descr", "BufferField" , "getDecimals" , null , true )) + list.put(prog.kw_decimals, execLib("cr_descr", "BufferField" , "getDecimals" , "setDecimals" , true )) list.put(prog.kw_def_comm, execLib("cr_descr", "Transaction" , "isDefaultCommit" , "setDefaultCommit" , true )) list.put(prog.kw_def_str , execLib("cr_descr", "BufferField" , "getDefaultString" , "setDefaultString" , true )) list.put(prog.kw_def_val , execLib("cr_descr", "BufferField" , "getDefaultValue" , "" , true )) @@ -711,14 +718,14 @@ list.put(prog.kw_end_f_d , execLib("cr_descr", "Droppable" , "endFileDrop" , null , true )) list.put(prog.kw_ent_tlst, execLib("cr_descr", "LogManager" , "getEntryTypesList" , null , true )) list.put(prog.kw_entry , execLib("cr_descr", "CommonListWidget" , "entry" , null , true )) - list.put(prog.kw_error , execLib("cr_descr", "Error" , "isError" , "setError" , true )) + list.put(prog.kw_error , execLib("cr_descr", "Error" , "error" , "error" , true )) list.put(prog.kw_err_str , execLib("cr_descr", "ErrorString" , "errorString" , "changeErrorString" , true )) list.put(prog.kw_evt_proc, execLib("cr_descr", "EventProcedure" , "getEventProcedure" , "setEventProcedure" , true )) list.put(prog.kw_expand , execLib("cr_descr", "RadioSet" , "isExpand" , "setExpand" , true )) list.put(prog.kw_export_p, execLib("cr_descr", "ClientPrincipal" , "exportPrincipal" , null , true )) list.put(prog.kw_f_key_h , execLib("cr_descr", "DataRelation" , "getForeignKeyHidden" , "setForeignKeyHidden" , true )) list.put(prog.kw_fetch_sr, execLib("cr_descr", "Browse" , "fetchSelectedRow" , null , true )) - list.put(prog.kw_fill_mod, execLib("cr_descr", "Buffer" , "fillMode" , "fillMode" , true )) + list.put(prog.kw_fill_mod, execLib("cr_descr", "Fillable" , "fillMode" , "fillMode" , true )) list.put(prog.kw_fill_wst, execLib("cr_descr", "DataSource" , "getFillWhereString" , "setFillWhereString" , true )) list.put(prog.kw_fill , execLib("cr_descr", "Fillable" , "fill" , null , true )) list.put(prog.kw_filled , execLib("cr_descr", "Rectangle" , "getFilled" , "setFilled" , true )) @@ -762,7 +769,7 @@ list.put(prog.kw_img_only, execLib("cr_descr", "ImageOnly" , "isImageOnly" , "setImageOnly" , true )) list.put(prog.kw_in_hndl , execLib("cr_descr", "Call" , "getInHandle" , "setInHandle" , true )) list.put(prog.kw_index , execLib("cr_descr", "Indexed" , "getIndex" , "setIndex" , true )) - list.put(prog.kw_init_c_p, execLib("cr_descr", "ClientPrincipal" , "initialize" , null , true )) + list.put(prog.kw_init_c_p, execLib("cr_descr", "ClientPrincipal" , "initialize" , null , true )) list.put(prog.kw_initiate, execLib("cr_descr", "Debugger" , "initiate" , null , true )) list.put(prog.kw_inner_c , execLib("cr_descr", "ScrollbarHorizontalElement" , "getInnerChars" , "setInnerChars" , true )) list.put(prog.kw_inner_l , execLib("cr_descr", "InnerLines" , "getInnerLines" , "setInnerLines" , true )) @@ -780,7 +787,7 @@ list.put(prog.kw_lab_font, execLib("cr_descr", "BrowseElement" , "getLabelFont" , "setLabelFont" , true )) list.put(prog.kw_labels , execLib("cr_descr", "Labels" , "isLabels" , "setLabels" , true )) list.put(prog.kw_large , execLib("cr_descr", "Editor" , "isLarge" , "setLarge" , true )) - list.put(prog.kw_last_bat, execLib("cr_descr", "Buffer" , "isLastBatch" , "setLastBatch" , true )) + list.put(prog.kw_last_bat, execLib("cr_descr", "Buffer" , "lastBatch" , "lastBatch" , true )) list.put(prog.kw_last_of , execLib("cr_descr", "Query" , "isLastOfGroup" , null , true )) list.put(prog.kw_last_ti , execLib("cr_descr", "FieldGroup" , "getLastTabItem" , "setLastTabItem" , true )) list.put(prog.kw_length , execLib("cr_descr", "Editor" , "getLength" , null , true )) @@ -819,17 +826,21 @@ list.put(prog.kw_merge_rc, execLib("cr_descr", "Buffer" , "mergeRowChanges" , null , true )) list.put(prog.kw_min_btn , execLib("cr_descr", "Window" , "isMinButton" , "setMinButton" , true )) list.put(prog.kw_min_val , execLib("cr_descr", "Slider" , "getMinValue" , "setMinValue" , true )) + list.put(prog.kw_min_schm, execLib("cr_descr", "TempTable" , "isMinSchemaMarshal" , "setMinSchemaMarshal" , true )) list.put(prog.kw_mnemon , execLib("cr_descr", "Mnemonic" , "getMnemonic" , null , true )) list.put(prog.kw_modified, execLib("cr_descr", "Widget" , "isModified" , "setModified" , true )) list.put(prog.kw_mou_ptr , execLib("cr_descr", "Widget" , "getMousePointer" , null , true )) list.put(prog.kw_mov_2eof, execLib("cr_descr", "Editor" , "moveCaretToEof" , null , true )) list.put(prog.kw_movable , execLib("cr_descr", "Widget" , "isMovable" , "setMovable" , true )) list.put(prog.kw_multiple, execLib("cr_descr", "Multiple" , "isMultiple" , "setMultiple" , true )) + list.put(prog.kw_namesp_p, execLib("cr_descr", "NamespaceURI" , "namespacePrefix" , "namespacePrefix" , true )) + list.put(prog.kw_namesp_u, execLib("cr_descr", "NamespaceURI" , "namespaceURI" , "namespaceURI" , true )) list.put(prog.kw_nested , execLib("cr_descr", "DataRelation" , "isNested" , "setNested" , true )) list.put(prog.kw_next_rid, execLib("cr_descr", "DataSource" , "getNextRowid" , "setNextRowid" , true )) list.put(prog.kw_no_cur_v, execLib("cr_descr", "Slider" , "isNoCurrentValue" , "setNoCurrentValue" , true )) list.put(prog.kw_no_em_sp, execLib("cr_descr", "Browse" , "isNoEmptySpace" , "setNoEmptySpace" , true )) list.put(prog.kw_no_focus, execLib("cr_descr", "Button" , "getNoFocus" , "setNoFocus" , true )) + list.put(prog.kw_no_sch_m, execLib("cr_descr", "TempTable" , "isNoSchemaMarshal" , "setNoSchemaMarshal" , true )) list.put(prog.kw_no_valid, execLib("cr_descr", "Browse" , "isNoValidate" , "setNoValidate" , true )) list.put(prog.kw_nodes , execLib("cr_descr", "Nodes" , "getNodes" , null , true )) list.put(prog.kw_num_buff, execLib("cr_descr", "BufferCollection" , "numBuffers" , null , true )) @@ -907,6 +918,7 @@ list.put(prog.kw_save_fil, execLib("cr_descr", "Editor" , "saveFile" , null , true )) list.put(prog.kw_save_rch, execLib("cr_descr", "Buffer" , "saveRowChanges" , null , true )) list.put(prog.kw_save_wst, execLib("cr_descr", "DataSource" , "getSaveWhereString" , "setSaveWhereString" , true )) + list.put(prog.kw_sch_mars, execLib("cr_descr", "TempTable" , "getSchemaMarshal" , "setSchemaMarshal" , true )) list.put(prog.kw_sing_run, execLib("cr_descr", "Procedure" , "isSingleRun" , null , true )) list.put(prog.kw_singlton, execLib("cr_descr", "Procedure" , "isSingleton" , null , true )) list.put(prog.kw_scr_2cr , execLib("cr_descr", "Browse" , "scrollToCurrentRow" , null , true )) @@ -922,7 +934,7 @@ list.put(prog.kw_selectbl, execLib("cr_descr", "Widget" , "isSelectable" , "setSelectable" , true )) list.put(prog.kw_selected, execLib("cr_descr", "Widget" , "isSelected" , "setSelected" , true )) list.put(prog.kw_sep_fgc , execLib("cr_descr", "Browse" , "getSeparatorFgColor" , "setSeparatorFgColor" , true )) - list.put(prog.kw_serialzh, execLib("cr_descr", "BufferField" , "getSerializeHidden" , "setSerializeHidden" , true )) + list.put(prog.kw_serialzh, execLib("cr_descr", "SerializeHiddenable" , "getSerializeHidden" , "setSerializeHidden" , true )) list.put(prog.kw_serialzn, execLib("cr_descr", "NamedSerializable" , "getSerializeName" , "setSerializeName" , true )) list.put(prog.kw_serialzr, execLib("cr_descr", "Buffer" , "serializeRow" , null , true )) list.put(prog.kw_server , execLib("cr_descr", "Remotable" , "getServerHandle" , "setServerHandle" , true )) @@ -973,7 +985,7 @@ list.put(prog.kw_xml_dtyp, execLib("cr_descr", "XmlNode" , "getXmlDataType" , "setXmlDataType" , true )) list.put(prog.kw_xml_nnam, execLib("cr_descr", "XmlNode" , "getXmlNodeName" , "setXmlNodeName" , true )) list.put(prog.kw_xml_ntyp, execLib("cr_descr", "XmlNode" , "getXmlNodeType" , "setXmlNodeType" , true )) - + list.put(prog.kw_add_bcca, execLib("cr_descr", "EmailSender" , "addBccAddress" , "" , true )) list.put(prog.kw_add_c_n , execLib("cr_descr", "TreeNodeCollection" , "addChildNode" , null , true )) @@ -1018,7 +1030,7 @@ list.put(prog.kw_caltitfg, execLib("cr_descr", "Calendar" , "getTitleForeColor" , "setTitleForeColor" , true )) list.put(prog.kw_caltrlfg, execLib("cr_descr", "Calendar" , "getTrailingForeColor" , "setTrailingForeColor" , true )) list.put(prog.kw_calupdwn, execLib("cr_descr", "Calendar" , "isUpDown" , "setUpDown" , true )) - list.put(prog.kw_calvalue, execLib("cr_descr", "Calendar" , "getDateTimeValue" , "setDateTimeValue" , true )) + list.put(prog.kw_calvalue, execLib("cr_descr", "Calendar" , "getCalendarValue" , "setCalendarValue" , true )) list.put(prog.kw_cease , execLib("cr_descr", "FWDTimer" , "cease" , null , true )) list.put(prog.kw_clea_tab, execLib("cr_descr", "Signature" , "clearTablet" , "" , true )) list.put(prog.kw_clea_win, execLib("cr_descr", "Signature" , "clearSignatureWindow" , "" , true )) @@ -1619,10 +1631,10 @@ methodText = "getResourceType" - ftype == prog.kw_error + ftype == prog.kw_error and ref.type == prog.sys_handle hwrap = "Error" methodText = "isError" - + isAssign methodText = "setError" @@ -3894,12 +3906,8 @@ - ftype == prog.kw_max_h_c - htype == prog.kw_browse - hwrap = "Browse" - hwrap = "Window" - + hwrap = "Window" methodText = "getMaxHeightChars" isAssign methodText = "setMaxHeightChars" @@ -3948,10 +3956,7 @@ ftype == prog.kw_min_h_c - htype == prog.kw_browse - hwrap = "Browse" - hwrap = "Window" - + hwrap = "MinHeightChars" methodText = "getMinHeightChars" isAssign methodText = "setMinHeightChars" @@ -4036,23 +4041,6 @@ methodText = "name" - - - ftype == prog.kw_namesp_p - hwrap = "XEntity" - methodText = "getNamespacePrefix" - isAssign - methodText = "setNamespacePrefix" - - - - ftype == prog.kw_namesp_u - hwrap = "NamespaceURI" - methodText = "getNamespaceURI" - isAssign - methodText = "setNamespaceURI" - - ftype == prog.kw_new methodText = "newlyCreated" === modified file 'rules/convert/variable_references.rules' --- rules/convert/variable_references.rules 2020-10-09 09:56:27 +0000 +++ rules/convert/variable_references.rules 2020-11-25 18:40:50 +0000 @@ -144,6 +144,7 @@ ** SBI 20200505 Added COM-SELF conversion support. ** RFB 20201002 Added additional parameter to thisProcedure when an annotation is found. Ref #4861 ** SVL 20201009 Emit extent fields where subscript is an expression. +** GES 20201125 Added OS-USERID as a FWD-specific 4GL extension. */ --> @@ -789,6 +790,9 @@ oldtype == prog.kw_os_err methodTxt = "FileSystemOps.getLastError" + oldtype == prog.kw_os_uid + methodTxt = "SecurityOps.getOSUserId" + oldtype == prog.kw_pro_arch methodTxt = "EnvironmentOps.getProcessArchitecture" === modified file 'rules/fixups/functions_procedures.rules' --- rules/fixups/functions_procedures.rules 2020-05-04 12:50:30 +0000 +++ rules/fixups/functions_procedures.rules 2020-12-19 00:59:30 +0000 @@ -7,7 +7,7 @@ ** ** Copyright (c) 2013-2020 Golden Code Development Corporation. ** -** _#_ _I_ __Date__ _________________________________Description_________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 CA 20130118 Added support for persistent procedures, procedure ** handles and super procedures. DYNAMIC-FUNCTION(expr IN ** handle) where expr is not a constant has the IN handle @@ -42,7 +42,8 @@ ** RFB 20200429 Removed a dumpTree that wasn't providing any useful information ** after the "WARNING: Hiding duplicate...". ** RFB 20200504 Shortened up some other log messages to still provide info, -** but not perform dumpTree(). +** but not perform dumpTree(). +** 012 OM 20201203 Fixed handling of READ-ONLY attributes. */ --> @@ -180,57 +181,40 @@ funcRetTypes.put(fname, cls) - - + + type == prog.assign ref = copy.getFirstChild() ref.type == prog.colon - - + ref = ref.getChildAt(1) - + ref.type == prog.db_ref_non_static ref = ref.nextSibling - + evalLib("read_only_attribute", ref) - - printfln("READ_ONLY_WARNING: REWRITING attribute assignment as string at %s line %05d column %05d\n%s\n", + printfln("READ_ONLY_WARNING: REWRITING attribute assignment as string at %s line %05d column %05d", file, line, - this.getColumn(), - this.dumpTree()) + this.getColumn()) - - pref = copy.getChildAt(1) - pref.type = prog.expression - - - pref = pref.getChildAt(0) - - pref.text = sprintf('"%s"', ref.text) - - pref.type = prog.string + + pref = createProgressAst(prog.expression, copy, 1) + pref.putAnnotation("read-only attribute helper", true) + pref = createProgressAst(prog.string, pref) + pref.text = sprintf('"%s"', ref.text) pref.putAnnotation("no_wrap", true) - - - pref.firstChild != null - ref = pref.firstChild - ref.remove() - + - + === modified file 'rules/gaps/expressions.rules' --- rules/gaps/expressions.rules 2020-09-10 09:54:53 +0000 +++ rules/gaps/expressions.rules 2021-01-13 21:04:41 +0000 @@ -185,7 +185,9 @@ ** SBI 20200520 LOAD-PICTURE is fully supported. ** 053 GES 20200628 Added SET-LASTKEY method and updated DEFAULT-STRING and DEFAULT-VALUE attributes. ** HC 20200726 Initial implementation of SPREADSHEET widget and related changes. -** 054 CA 20200910 Added BUFFER:SERIALIZE-ROW() method conversion as FULL. +** CA 20200910 Added BUFFER:SERIALIZE-ROW() method conversion as FULL. +** GES 20201125 Added OS-USERID as a FWD-specific 4GL extension. +** OM 20201120 Updated gaps for multiple methods/attributes related to datasets and xml serialization. */ --> @@ -377,7 +379,7 @@ funcs.put(prog.kw_cur_res , rw.cvt_lvl_full | rw.rt_lvl_full) funcs.put(prog.kw_cur_val , rw.cvt_lvl_full | rw.rt_lvl_full) funcs.put(prog.kw_cvt_dt , rw.cvt_lvl_none | rw.rt_lvl_none) - funcs.put(prog.kw_data_sm , rw.cvt_lvl_full | rw.rt_lvl_full) + funcs.put(prog.kw_data_sm , rw.cvt_lvl_full | rw.rt_lvl_full) funcs.put(prog.kw_date , rw.cvt_lvl_full | rw.rt_lvl_full) funcs.put(prog.kw_date_tz , rw.cvt_lvl_full | rw.rt_lvl_full) funcs.put(prog.kw_datetime, rw.cvt_lvl_full | rw.rt_lvl_full) @@ -600,6 +602,7 @@ vars.put(prog.kw_opsys , rw.cvt_lvl_full | rw.rt_lvl_full) vars.put(prog.kw_os_drv , rw.cvt_lvl_full | rw.rt_lvl_full) vars.put(prog.kw_os_err , rw.cvt_lvl_full | rw.rt_lvl_full) + vars.put(prog.kw_os_uid , rw.cvt_lvl_full | rw.rt_lvl_full) vars.put(prog.kw_pro_arch, rw.cvt_lvl_full | rw.rt_lvl_full) vars.put(prog.kw_proc_hnd, rw.cvt_lvl_none | rw.rt_lvl_none) vars.put(prog.kw_proc_st , rw.cvt_lvl_none | rw.rt_lvl_none) @@ -819,7 +822,7 @@ attrs.put(prog.kw_data_e_r, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_data_scm, rw.cvt_lvl_full | rw.rt_lvl_stub) attrs.put(prog.kw_dataset , rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_data_sm , rw.cvt_lvl_full | rw.rt_lvl_partial) + attrs.put(prog.kw_data_sm , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_data_src, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_data_sri, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_datatype, rw.cvt_lvl_full | rw.rt_lvl_full) @@ -931,8 +934,8 @@ attrs.put(prog.kw_fil_c_t , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_fill , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_filled , rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_fill_mod, rw.cvt_lvl_full | rw.rt_lvl_full_restr) - attrs.put(prog.kw_fill_wst, rw.cvt_lvl_full | rw.rt_lvl_full) + attrs.put(prog.kw_fill_mod, rw.cvt_lvl_full | rw.rt_lvl_full) + attrs.put(prog.kw_fill_wst, rw.cvt_lvl_full | rw.rt_lvl_partial) attrs.put(prog.kw_fil_m_d , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_fil_m_t , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_fil_name, rw.cvt_lvl_full | rw.rt_lvl_full) @@ -1143,7 +1146,7 @@ attrs.put(prog.kw_language, rw.cvt_lvl_none | rw.rt_lvl_none) attrs.put(prog.kw_large , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_last_ar , rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_last_bat, rw.cvt_lvl_full | rw.rt_lvl_stub) + attrs.put(prog.kw_last_bat, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_last_ch , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_last_frm, rw.cvt_lvl_none | rw.rt_lvl_none) attrs.put(prog.kw_last_obj, rw.cvt_lvl_full | rw.rt_lvl_full) @@ -1217,7 +1220,7 @@ attrs.put(prog.kw_menu_mou, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_merge_bf, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_merge_ch, rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_merge_rc, rw.cvt_lvl_full | rw.rt_lvl_stub) + attrs.put(prog.kw_merge_rc, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_min_btn , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_min_cwch, rw.cvt_lvl_none | rw.rt_lvl_none) attrs.put(prog.kw_min_cwpx, rw.cvt_lvl_none | rw.rt_lvl_none) @@ -1264,6 +1267,7 @@ attrs.put(prog.kw_nodv_2lc, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_no_em_sp, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_no_focus, rw.cvt_lvl_full | rw.rt_lvl_full) + attrs.put(prog.kw_no_pre, rw.cvt_lvl_full | rw.rt_lvl_full_restr) attrs.put(prog.kw_normalze, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_no_sch_m, rw.cvt_lvl_none | rw.rt_lvl_none) attrs.put(prog.kw_no_valid, rw.cvt_lvl_full | rw.rt_lvl_full) @@ -1366,8 +1370,8 @@ attrs.put(prog.kw_read_fil, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_read_jsn, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_read_onl, rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_read_xml, rw.cvt_lvl_full | rw.rt_lvl_partial) - attrs.put(prog.kw_read_xsc, rw.cvt_lvl_full | rw.rt_lvl_stub) + attrs.put(prog.kw_read_xml, rw.cvt_lvl_full | rw.rt_lvl_partial) + attrs.put(prog.kw_read_xsc, rw.cvt_lvl_full | rw.rt_lvl_partial) attrs.put(prog.kw_recid , rw.cvt_lvl_full | rw.rt_lvl_full_restr) attrs.put(prog.kw_rec_len , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_recurse , rw.cvt_lvl_full | rw.rt_lvl_stub) @@ -1392,7 +1396,7 @@ attrs.put(prog.kw_repos_2i, rw.cvt_lvl_full_restr | rw.rt_lvl_full_restr) attrs.put(prog.kw_repos_2r, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_repos_b , rw.cvt_lvl_full | rw.rt_lvl_basic) - attrs.put(prog.kw_repos , rw.cvt_lvl_full | rw.rt_lvl_partial) + attrs.put(prog.kw_repos , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_repos_f , rw.cvt_lvl_full | rw.rt_lvl_basic) attrs.put(prog.kw_rep_stxt, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_req_info, rw.cvt_lvl_none | rw.rt_lvl_full) @@ -1449,7 +1453,7 @@ attrs.put(prog.kw_seal , rw.cvt_lvl_full | rw.rt_lvl_partial) attrs.put(prog.kw_seal_tst, rw.cvt_lvl_full | rw.rt_lvl_stub) attrs.put(prog.kw_search , rw.cvt_lvl_full | rw.rt_lvl_full_restr) - attrs.put(prog.kw_sel_all , rw.cvt_lvl_full | rw.rt_lvl_full) + attrs.put(prog.kw_sel_all , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_selectbl, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_selected, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_sel_end , rw.cvt_lvl_full | rw.rt_lvl_full) @@ -1463,7 +1467,7 @@ attrs.put(prog.kw_sep_fgc , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_seps , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_serialzh, rw.cvt_lvl_full | rw.rt_lvl_partial) - attrs.put(prog.kw_serialzn, rw.cvt_lvl_full | rw.rt_lvl_partial) + attrs.put(prog.kw_serialzn, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_serialzr, rw.cvt_lvl_full | rw.rt_lvl_stub) attrs.put(prog.kw_server , rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_sess_end, rw.cvt_lvl_none | rw.rt_lvl_none) @@ -1475,8 +1479,8 @@ attrs.put(prog.kw_set_blue, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_set_brk , rw.cvt_lvl_full | rw.rt_lvl_stub) attrs.put(prog.kw_set_buf , rw.cvt_lvl_full | rw.rt_lvl_full) - attrs.put(prog.kw_set_cbac, rw.cvt_lvl_full | rw.rt_lvl_partial) - attrs.put(prog.kw_set_cb_p, rw.cvt_lvl_full | rw.rt_lvl_partial) + attrs.put(prog.kw_set_cbac, rw.cvt_lvl_full | rw.rt_lvl_full) + attrs.put(prog.kw_set_cb_p, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_set_clnt, rw.cvt_lvl_full | rw.rt_lvl_basic) attrs.put(prog.kw_set_comm, rw.cvt_lvl_full | rw.rt_lvl_full) attrs.put(prog.kw_set_c_p , rw.cvt_lvl_full | rw.rt_lvl_full) === modified file 'rules/gaps/lang_stmts.rules' --- rules/gaps/lang_stmts.rules 2020-03-31 19:34:28 +0000 +++ rules/gaps/lang_stmts.rules 2020-12-16 15:49:52 +0000 @@ -36,6 +36,7 @@ ** GES 20191015 More updates for OO and structured error handling. ** 022 HC 20200331 Implemented support for OPEN-POPUP legacy method, which opens popup menu set ** on the widget. +** 023 EVL 20201216 Added function to interrupt metafile recording. */ --> @@ -261,6 +262,7 @@ opts.put(prog.kw_mf_gxy, rw.cvt_lvl_partial | rw.rt_lvl_basic) opts.put(prog.kw_mf_gzf, rw.cvt_lvl_partial | rw.rt_lvl_basic) opts.put(prog.kw_mf_init, rw.cvt_lvl_partial | rw.rt_lvl_basic) + opts.put(prog.kw_mf_ir, rw.cvt_lvl_partial | rw.rt_lvl_basic) opts.put(prog.kw_mf_mpdf, rw.cvt_lvl_partial | rw.rt_lvl_basic) opts.put(prog.kw_mf_p2mu, rw.cvt_lvl_partial | rw.rt_lvl_basic) opts.put(prog.kw_mf_rp, rw.cvt_lvl_partial | rw.rt_lvl_basic) === modified file 'rules/include/common-progress.rules' --- rules/include/common-progress.rules 2020-09-30 11:51:44 +0000 +++ rules/include/common-progress.rules 2021-01-13 21:04:41 +0000 @@ -6,7 +6,7 @@ ** ** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ __JPRM__ ____________________________Description_____________________________ +** _#_ _I_ __Date__ __JPRM__ __________________________________Description__________________________________ ** 001 GES 20050721 @21761 Good working version which provides many callable functions/rules ** that allow the centralized definition and reuse of complex ** expressions, logic and processing. @@ -440,6 +440,7 @@ ** GES 20200521 Added legacy name overrides. ** 236 CA 20200803 Added create_temp_table_key_criteria. ** 237 IAS 20200809 Added support for the MAX-WIDTH FIELD attribute. +** OM 20201120 The DECIMALS attribute is writable for TEMP-TABLES but not for permanent tables. */ --> @@ -6881,39 +6882,38 @@ - + - - ftype = #(int) (#(long) ref.getAnnotation("oldtype")) - + ftype = #(int) (#(long) ref.getAnnotation("oldtype")) - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -6998,7 +6998,6 @@ ftype == prog.kw_dde_id || ftype == prog.kw_dde_name || ftype == prog.kw_dde_topi || - ftype == prog.kw_decimals || ftype == prog.kw_def_bufh || ftype == prog.kw_def_val || ftype == prog.kw_disp_typ || === modified file 'rules/schema/dmo_common.rules' --- rules/schema/dmo_common.rules 2020-10-22 11:53:30 +0000 +++ rules/schema/dmo_common.rules 2021-01-31 10:14:52 +0000 @@ -124,15 +124,20 @@ ** SBI 20190728 Added "help" annotation. ** 056 OM 20190831 Added hidden fields and index specific to BEFORE TEMP-TABLES. ** 057 IAS 20200601 Added support for the table/field string attributes (*-SA properties). -** 058 OM 20200108 Moved back the extent support method definition to main interface. +** 058 IAS 20200604 Added support for the table/filed 'can*' attributes and table 'valexp' attr +** 059 OM 20200108 Moved back the extent support method definition to main interface. ** Added support for "dirty-read" hint/annotation. ** Removed generation of DMO implementation classes. -** 059 IAS 20200604 Added support for the table/filed 'can*' attributes and table 'valexp' attributes +** 060 IAS 20200604 Added support for the table/filed 'can*' attributes and table 'valexp' attributes ** IAS 20200624 Added support for the field VIEW-AS attributes ** IAS 20200809 Added support for the field DECIMALS, DESCRIPTION, CASE-SENSITIVE, and ** MAX-WIDTH attributes -** 060 OM 20200925 Fixed latent issue. adjustDataType() returned null in some cases. +** OM 20200925 Fixed latent issue. adjustDataType() returned null in some cases. ** CA 20201021 Fixed runtime conversion of dynamic temp-tables with Progress.Lang.Object fields. +** SVL 20201111 Raise error if LABEL/COLUMN-LABEL matches reserved FWD 'null string'. +** OM 20201120 Added "no-undo" annotation. +** IAS 20201203 Added generation database object for the word tables' support. +** IAS 20201219 Added 'CPSTREAM' import task argument --> + copy.isAnnotation("no-undo-tt") + tpl.graft("annotation_assign_true", null, tableAst, "key", "noUndo") + + copy.isAnnotation("serialize-name") @@ -951,15 +961,13 @@ - !isWord - idxName = idxAst.getAnnotation("historical") - - !idxAst.parent.isAnnotation("drop") - - ddl.addIndex( - ddlSchema, legacyTable, idxName, idxAst.text, isPrimary, isUnique, isWord) - - + idxName = idxAst.getAnnotation("historical") + + !idxAst.parent.isAnnotation("drop") + + ddl.addIndex( + ddlSchema, legacyTable, idxName, idxAst.text, isPrimary, isUnique, isWord) + @@ -971,8 +979,9 @@ idxCompAst != null ref = getAst(#(long) idxCompAst.getAnnotation("refid")) - - + + + isDesc = idxCompAst.isAnnotation("descend") and #(boolean) idxCompAst.getAnnotation("descend") @@ -985,17 +994,15 @@ 'legacy_name', legacyName) !ref.parent.isAnnotation("drop") - !isWord - - ddl.addIndexComponent(ddlSchema, legacyTable, idxName, ref.text, - #(java.lang.String) ref.getAnnotation("column"), - legacyName, - ref.getAnnotation("datatype").equals("character"), - ref.isAnnotation("case-sensitive") and - #(boolean) ref.getAnnotation("case-sensitive"), - isDesc) - - + + ddl.addIndexComponent(ddlSchema, legacyTable, idxName, ref.text, + #(java.lang.String) ref.getAnnotation("column"), + legacyName, + ref.getAnnotation("datatype").equals("character"), + ref.isAnnotation("case-sensitive") and + #(boolean) ref.getAnnotation("case-sensitive"), + isDesc) + isDesc @@ -1009,6 +1016,17 @@ idxCompAst = idxAst.getImmediateChild(data.index_col, idxCompAst) + + isWord + + wordTableName = ddl.getWordTableName(ddlSchema, legacyTable, idxName) + + + tpl.graft('annotation_assign_string', null, primaryAst, + 'key', 'wordtablename', + 'value', wordTableName) + + idxAst = copy.getImmediateChild(data.index, idxAst) @@ -1579,7 +1597,7 @@ - + newDMO @@ -1690,6 +1708,7 @@ sourceAst.isAnnotation("col_lab") tmpString = sourceAst.getAnnotation('col_lab') + execLib("checkReservedNull", tmpString, "COLUMN-LABEL") tpl.graft('annotation_assign_string', null, fieldAst, 'key', 'columnLabel', @@ -1720,6 +1739,7 @@ sourceAst.isAnnotation("label") tmpString = sourceAst.getAnnotation('label') + execLib("checkReservedNull", tmpString, "LABEL") tpl.graft('annotation_assign_string', null, fieldAst, 'key', 'label', @@ -2452,7 +2472,7 @@ hiberProps = execLib("buildDbImportProperties", - dbName, targetDb, null, null, null) + dbName, targetDb, null, null, null, null) config.addProperties(hiberProps) @@ -2463,6 +2483,18 @@ p2jDatabase = create("com.goldencode.p2j.persist.Database", dbName) + + + + + + + param.equals("__NULL_STRING__#!^+*__") + throwException(sprintf("ERROR: By miraculous coincidence in-app %s + matches reserved FWD 'null string' constant '__NULL_STRING__#!^+*__'. Update + this constant in FWD.", paramType)) + + === modified file 'rules/schema/generate_ddl.xml' --- rules/schema/generate_ddl.xml 2020-09-23 23:47:02 +0000 +++ rules/schema/generate_ddl.xml 2021-01-13 21:04:41 +0000 @@ -5,7 +5,7 @@ ** ** Copyright (c) 2006-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ _________________________________Description_________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 GES 20100629 Created initial version based on reindex.xml. Walks P2O ** AST to setup Hibernate, then uses Hibernate to generate ** DDL for all tables and for all indexes. @@ -35,6 +35,7 @@ ** 015 VMN 20140502 Added enhance schema name conversion support for hint "escape" attribute. ** 016 OM 20160712 Removed p2j_id_generator_sequence from generated index DDL. ** 017 OM 20200906 New ORM implementation. +** 018 OM 20201231 The index components is aware of all their name (legacy/orm/sql). --> - @@ -119,13 +119,12 @@ - type == data.property and this.isAnnotation('composite') + type == data.property and this.isAnnotation("extent") dataType = execLib('getDataType', this) dType = execLib('adjustDataType', dataType) - refAst = getAst(#(long) this.getAnnotation('composite')) - extentVal = refAst.getAnnotation('extent') + extentVal = this.getAnnotation("extent") descList = execLib('gatherDocData', this) === modified file 'rules/schema/import.xml' --- rules/schema/import.xml 2020-09-23 23:47:02 +0000 +++ rules/schema/import.xml 2021-01-25 07:55:58 +0000 @@ -97,6 +97,8 @@ ** 030 OM 20200906 New ORM implementation. ** 031 CA 20200910 Fixed the datetime(-tz) field's initial value at import - it uses ISO8601 ** format or NOW function. +** IAS 20201219 Added 'CPSTREAM' import task argument +** Added word tables population on import --> - - - - - + + + + + + @@ -279,7 +282,7 @@ printfln('INFO: Using %d threads for import', maxThreads) cacheables = create('java.util.HashSet') - ormProps = execLib("buildDbImportProperties", dbName, targetDb, url, uid, pw) + ormProps = execLib("buildDbImportProperties", dbName, targetDb, url, uid, pw, cpstream) fwdDialect = create(ormProps.getProperty("dialect")) @@ -363,6 +366,7 @@ + schemaName = text @@ -441,6 +445,14 @@ type == data.index and !skipImport + + wordTableName = null + + this.isAnnotation("word") + + wordTableName = imp.getWordTableName(dmoIface, text) + + indexDef = create("com.goldencode.p2j.persist.P2JIndex", @@ -448,7 +460,8 @@ data.formatWithEscapedParameter("idx__%s", text), this.isAnnotation("unique"), this.isAnnotation("primary"), - this.isAnnotation("word")) + this.isAnnotation("word"), + wordTableName) === modified file 'rules/schema/import_util.rules' --- rules/schema/import_util.rules 2020-07-13 06:22:42 +0000 +++ rules/schema/import_util.rules 2020-12-23 16:58:23 +0000 @@ -19,6 +19,7 @@ ** 007 VMN 20131106 Removed "sqlserver2008" target DB because SQL Server 2008 does not ** support sequences. ** 008 OM 20200408 Replaced Hibernate with customized orm. +** 009 IAS 20201219 Added 'CPSTREAM' import task argument --> + + + @@ -159,6 +163,10 @@ pw = "p3rs1st" + cpstream != null + >dbImportParams.put("dump.cpstream", cpstream) + + dbImportParams.put("dialect", dialectClass) dbImportParams.put("database_name", dbName) dbImportParams.put("connection.driver_class", driverClass) === modified file 'rules/schema/index_test.xml' --- rules/schema/index_test.xml 2020-09-23 23:47:02 +0000 +++ rules/schema/index_test.xml 2021-01-13 21:04:41 +0000 @@ -185,7 +185,8 @@ !indexDef.isUnique() indexDef.addComponent( - this.getImmediateChild(data.primary, null).getAnnotation('column').toString()) + this.getImmediateChild(data.primary, null) + .getAnnotation('column').toString()) === modified file 'rules/schema/java_dmo.xml' --- rules/schema/java_dmo.xml 2020-10-22 11:53:30 +0000 +++ rules/schema/java_dmo.xml 2021-01-31 10:14:52 +0000 @@ -35,6 +35,7 @@ ** Dropped support for generation of DMO Impls. ** OM 20200610 Generated the _Sequences enum. ** 016 CA 20201021 Fixed runtime conversion of dynamic temp-tables with Progress.Lang.Object fields. +** IAS 20201203 Added generation database object for the word tables' support. --> ddl.generateIndexDDLs(ddlSchema) + + ddl.generateWordTablesDDLs(ddlSchema) === modified file 'rules/schema/metaschema.xml' --- rules/schema/metaschema.xml 2020-09-07 16:23:31 +0000 +++ rules/schema/metaschema.xml 2021-01-13 21:04:41 +0000 @@ -5,7 +5,7 @@ ** ** Copyright (c) 2013-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ _________________________________Description__________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 ECF 20130515 Created initial version. ** 002 ECF 20131028 Exported more fields. ** 003 VMN 20140202 Added support for shift of order on custom denormalization of fields with @@ -28,6 +28,7 @@ ** 015 ECF 20200906 New ORM implementation. ** 016 IAS 20200624 Added support for the field VIEW-AS attributes ** IAS 20200809 Added support for the field DECIMALS, DESCRIPTION, CASE-SENSITIVE and MAX-WIDTH attributes +** OM 20201120 Dropped deprecated 'newName' pseudo-field/property. --> - execLib("addField", "newName", text, "c") - @@ -477,7 +475,6 @@ newName = text newName = getNoteString("denormalizedpropertyname") - execLib("addField", "newName", newName, "c") execLib("addField", "dataType", getNoteString("datatype"), "c") === modified file 'rules/schema/p2o.xml' --- rules/schema/p2o.xml 2020-10-06 07:23:27 +0000 +++ rules/schema/p2o.xml 2021-01-13 21:04:41 +0000 @@ -267,10 +267,11 @@ ** this was fixed so that the .schema AST is a real .schema, and not .p2o. ** OM 20200925 Added support for generating denormalized extent properties in dynamic ** conversion of temp-tables. -** 098 CA 20201005 Fixed incremental conversion for temp-tables - nextTTSuffix must be inherited from -** the previous, existing, peer, when a file adds usage for a temp-table already -** defined in another file. -** Fixed nextTTSuffix for SHARED TEMP-TABLE without a NEW SHARED. +** CA 20201005 Fixed incremental conversion for temp-tables - nextTTSuffix must be inherited +** from the previous, existing, peer, when a file adds usage for a temp-table +** already defined in another file. +** Fixed nextTTSuffix for SHARED TEMP-TABLE without a NEW SHARED. +** OM 20201120 Added NO-UNDO attribute / temp-table option support. --> tmpTabNodes.put(tmpTabKey, peer) === modified file 'rules/schema/p2o_post.xml' --- rules/schema/p2o_post.xml 2020-09-23 23:47:02 +0000 +++ rules/schema/p2o_post.xml 2021-01-13 21:04:41 +0000 @@ -5,12 +5,13 @@ ** ** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** _#_ _I_ __Date__ _________________________________Description__________________________________ +** _#_ _I_ __Date__ _______________________________________Description_______________________________________ ** 001 ECF 20150326 Refactored later rule-sets from p2o.xml to here. This is necessary because ** we are dependent upon peer nodes being available for cross-schema joins. ** 002 AIL 20190722 Added rule-sets for triggers. ** 003 OM 20190826 The BEFORE-BUFFERs do not inherit the indexes of their parent AFTER-BUFFER. ** 004 ECF 20200601 Fixed typo. +** 005 OM 20201231 The index components is aware of all their name (legacy/orm/sql). --> + desc}, {@code desc-->asc}); + * else {@code false}. * @param sortCrit - * List of SortCriterion objects appropriate to the - * database dialect. + * List of {@code SortCriterion} objects appropriate to the database dialect. * * @return An HQL compliant order by clause. */ - private String buildSortClause(boolean invert, - List sortCrit) + private String buildSortClause(boolean invert, List sortCrit) { StringBuilder buf = new StringBuilder("order by "); - // Inject multiplex ID for temp table before primary key, so as to - // match the way these components are ordered in the table's index. + // inject multiplex ID for temp table before primary key, so as to match the way these components are + // ordered in the table's index. if (multiplexSort) { injectMultiplexSort(invert, buf); buf.append(", "); } - + int i = 0; Iterator iter = sortCrit.iterator(); for (; iter.hasNext(); i++) @@ -1020,7 +1012,7 @@ buf.append(next.toSortExpression(invert)); } - + return buf.toString(); } === modified file 'src/com/goldencode/p2j/persist/HQLPreprocessor.java' --- src/com/goldencode/p2j/persist/HQLPreprocessor.java 2020-10-01 17:13:26 +0000 +++ src/com/goldencode/p2j/persist/HQLPreprocessor.java 2020-11-20 18:40:59 +0000 @@ -6059,7 +6059,7 @@ } } - /** A public container for storing a pair of properties. It is immutable, used for transfering data. */ + /** A public container for storing a pair of properties. It is immutable, used for transferring data. */ public static final class PropertyPair { /** Operator used for joining the properties. */ === modified file 'src/com/goldencode/p2j/persist/IndexHelper.java' --- src/com/goldencode/p2j/persist/IndexHelper.java 2020-09-23 23:47:02 +0000 +++ src/com/goldencode/p2j/persist/IndexHelper.java 2020-10-14 21:06:19 +0000 @@ -46,6 +46,8 @@ ** 022 ECF 20200906 New ORM implementation. ** 023 OM 20200919 Improved access to dmo metadata. ** OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them. +** OM 20201008 Detected direction of used index and stored for further access. +** OM 20201012 Force use locally cached meta information instead of map lookup. */ /* @@ -109,6 +111,7 @@ import com.goldencode.p2j.persist.lock.*; import com.goldencode.p2j.persist.orm.*; import com.goldencode.p2j.util.*; +import org.apache.commons.lang3.tuple.*; /** * Helper object to manage database index information. One instance of this @@ -139,14 +142,17 @@ /** Logger */ private static final Logger LOG = LogHelper.getLogger(IndexHelper.class); + /** Is FINE level logging allowed? */ + private static final boolean LOG_FINE = LOG.isLoggable(Level.FINE); + /** Cache of instances, mapped by database */ private static final Map cache = new HashMap<>(); /** Primary (non-dirty) database with which this helper is associated */ private final Database database; - /** Map of sort clauses to index names */ - private final Map indexBySort = new HashMap<>(); + /** Map of sort clauses to pair of index names and their direction ({@code false} means reversed). */ + private final Map> indexBySort = new HashMap<>(); /** Map of property names to lists of indexes containing them */ private final Map> indexesByProperty = new HashMap<>(); @@ -161,8 +167,7 @@ private final Map primaryIndexByEntity = new HashMap<>(); /** Map of entity keys to maps of index names to index property names */ - private final Map>> indexMapsByEntity = - new HashMap<>(); + private final Map>> indexMapsByEntity = new HashMap<>(); /** Map of entity keys to indexed properties */ private final Map> propsByEntity = new HashMap<>(); @@ -287,18 +292,21 @@ String index = null; String entity = buffer.getEntityName(); SortKey key = new SortKey(entity, sort); + Pair indexAndDir; synchronized (indexBySort) { - index = indexBySort.get(key); - if (index == null) - { - index = lookupIndexForSort(buffer, sort); - } - else if ("".equals(index)) - { - index = null; - } + indexAndDir = indexBySort.get(key); + if (indexAndDir == null) + { + indexAndDir = lookupIndexForSort(buffer, sort); + } + } + + index = indexAndDir.getLeft(); + if ("".equals(index)) + { + index = null; } return index; @@ -384,40 +392,61 @@ } /** - * Retrieve a {@link DMOSorter} instance which is able to sort DMO - * instances according to the sort criteria specified by the given sort - * phrase. + * Retrieve a {@link DMOSorter} instance which is able to sort DMO instances according to the sort criteria + * specified by the given sort phrase. * * @param buffer - * Record buffer which contains information about the DMO entity - * associated with the sort phrase. + * Record buffer which contains information about the DMO entity associated with the sort phrase. * @param sort - * A sort phrase which includes one or more DMO property - * references, each qualified by a DMO alias and followed by a - * keyword indicating sort direction. + * A sort phrase which includes one or more DMO property references, each qualified by a DMO alias + * and followed by a keyword indicating sort direction. * - * @return DMO sorter object which can sort DMOs according to the given - * index. + * @return DMO sorter object which can sort DMOs according to the given index. * * @throws PersistenceException - * if there is any error querying index metadata from the JDBC - * driver of the associated database. + * if there is any error querying index metadata from the JDBC driver of the associated database. */ public DMOSorter getSorterForSortPhrase(RecordBuffer buffer, String sort) throws PersistenceException { + Pair indexAndDir; SortKey key = new SortKey(buffer.getEntityName(), sort); synchronized (indexBySort) { - String index = indexBySort.get(key); - if (index == null) + indexAndDir = indexBySort.get(key); + if (indexAndDir == null) { - // This will add an appropriate sorter to the sorters map, if possible. + // this will add an appropriate sorter to the sorters map, if possible lookupIndexForSort(buffer, sort); } - - return sorters.get(key); + } + + return sorters.get(key); + } + + /** + * Obtain the direction of the index used to sort the buffer by a specified sort phrase. + * + * @param buffer + * Record buffer which contains information about the DMO entity associated with the sort phrase. + * @param sort + * A sort phrase which includes one or more DMO property references, each qualified by a DMO alias + * and followed by a keyword indicating sort direction. + * + * @return {@code true} if the direction is the same as the index, {@code false} if the direction is + * inversed (I.e. the result set is reversed compared to the index) and {@code null} if the sort + * phrase does not benefit from any index, or the sort phrase was not processed yet for this + * buffer. + */ + public Boolean getIndexDirection(RecordBuffer buffer, String sort) + { + SortKey key = new SortKey(buffer.getEntityName(), sort); + + synchronized (indexBySort) + { + Pair indexAndDir = indexBySort.get(key); + return indexAndDir == null ? null : indexAndDir.getRight(); } } @@ -651,7 +680,7 @@ list.trimToSize(); } - if (LOG.isLoggable(Level.FINE)) + if (LOG_FINE) { LOG.log(Level.FINE, "Indexes " + (unique ? "[unique]" : "[all]") + " for entity '" + entity + "': " + list); @@ -734,7 +763,7 @@ } } - if (LOG.isLoggable(Level.FINE)) + if (LOG_FINE) { LOG.log(Level.FINE, "Indexes " + (unique ? "[unique]" : "[non-unique]") + @@ -796,7 +825,7 @@ list.trimToSize(); } - if (LOG.isLoggable(Level.FINE)) + if (LOG_FINE) { LOG.log(Level.FINE, "Indexes for '" + entity + ":" + property + "': " + list); } @@ -824,10 +853,13 @@ * if there is any error querying index metadata from the JDBC driver of the * associated database. */ - private String lookupIndexForSort(RecordBuffer buffer, String sort) + private Pair lookupIndexForSort(RecordBuffer buffer, String sort) throws PersistenceException { + Pair ret = null; String indexName = null; + Boolean direction = null; + int badDirection = -1; SortKey key = new SortKey(buffer.getEntityName(), sort); Iterator iter = buffer.dmoInfo.getDatabaseIndexes(); Class dmoIface = buffer.dmoInfo.getAnnotatedInterface(); @@ -839,6 +871,9 @@ while (iter.hasNext()) { boolean match = true; + direction = null; + int i = 0; + badDirection = -1; P2JIndex index = iter.next(); Iterator comps = index.components(true); Iterator crits = scList.iterator(); @@ -868,20 +903,46 @@ match = false; break; } + + i++; + boolean sameDir = next.isDescending() != crit.isAscending(); + if (direction == null) + { + // detect index direction (whether the order given by sort is the same as the index) + direction = sameDir; + } + else + { + // just test whether the direction is kept and the index is a match (even if completely + // reversed). If a discrepancy is detected a warning is issued, but the index is accepted + // TODO: in the latter case there is probably a programming error. The index will not be + // matched server-side and the query will be slow! + if (direction != sameDir && badDirection == -1) + { + badDirection = i; + } + } } if (match) { indexName = index.getName().intern(); - indexBySort.put(key, indexName); + ret = Pair.of(indexName, direction); sorters.put(key, new DMOSorter(buffer.getDatabase(), dmoIface, scList)); + if (badDirection != -1 && LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, + "The sort properties (" + sort + ") match the index components (" + index + + ") but the direction of " + badDirection + " component is not consistent with " + + "the first one. The index will NOT be used!"); + } break; } } } - if (LOG.isLoggable(Level.FINE)) + if (LOG_FINE) { LOG.log(Level.FINE, "Index for sort phrase '" + sort + "': " + @@ -890,10 +951,11 @@ if (indexName == null) { - indexBySort.put(key, ""); + ret = Pair.of("", true); } - return indexName; + indexBySort.put(key, ret); + return ret; } /** @@ -919,9 +981,10 @@ throws PersistenceException { Set union = null; - Class dmoClass = DBUtils.dmoClassForEntity(entity); - Iterator iter = DmoMetadataManager.getDmoInfo(dmoClass).getDatabaseIndexes(); - Map mappedCols = DatabaseManager.getColumnToPropertyMap(database, dmoClass, null, true); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null); + Iterator iter = dmoInfo.getDatabaseIndexes(); + Map mappedCols = DatabaseManager.getColumnToPropertyMap( + database, dmoInfo.getImplementationClass(), null, true); if (iter == null) { @@ -979,7 +1042,7 @@ } } - if (LOG.isLoggable(Level.FINE)) + if (LOG_FINE) { LOG.log(Level.FINE, "Indexed properties " + (unique ? "[unique]" : "[all]") + === modified file 'src/com/goldencode/p2j/persist/P2JIndex.java' --- src/com/goldencode/p2j/persist/P2JIndex.java 2020-09-25 16:00:36 +0000 +++ src/com/goldencode/p2j/persist/P2JIndex.java 2021-01-25 07:55:58 +0000 @@ -23,6 +23,7 @@ ** 012 ECF 20150201 Added components(boolean) method. ** 013 ECF 20200906 New ORM implementation. ** 014 OM 20200925 Separated namespaces for index components. +** 015 IAS 20201224 Added word tables support */ /* @@ -143,6 +144,9 @@ /** Does this index define a unique constraint for its table? */ private boolean unique; + /** Word table name (for word index)*/ + private String wordTableName; + /** * Convenience constructor. * @@ -176,13 +180,44 @@ * true if this index is a word index, else * false. */ - public P2JIndex(String table, String name, boolean unique, boolean primary, boolean word) + public P2JIndex(String table, String name, boolean unique, + boolean primary, boolean word) + { + this(table, name, unique, primary, word, null); + } + + /** + * Convenience constructor. + * + * @param table + * Name of the table with which the index is associated. + * @param name + * Index name. + * @param unique + * true if this index defines a unique constraint + * for its table, else false. + * @param primary + * true if this index is the primary index for its + * table, else false. + * @param word + * true if this index is a word index, else + * false. + * @param wordTableName + * Word table name (only used for word index). + */ + public P2JIndex(String table, + String name, + boolean unique, + boolean primary, + boolean word, + String wordTableName) { this.table = table; this.name = name; this.unique = unique; this.primary = primary; this.word = word; + this.wordTableName = wordTableName; } /** @@ -861,6 +896,27 @@ } /** + * Get word table names by dialect + * + * @return Word table name. + */ + public String getWordTableName() + { + return wordTableName; + } + + /** + * Set word table names + * + * @param wordTableNames + * Word table names by dialect. + */ + public void setWordTableName(String wordTableName) + { + this.wordTableName = wordTableName; + } + + /** * Set the table with which this index is associated. * * @param table === modified file 'src/com/goldencode/p2j/persist/P2JQuery.java' --- src/com/goldencode/p2j/persist/P2JQuery.java 2020-09-18 09:44:42 +0000 +++ src/com/goldencode/p2j/persist/P2JQuery.java 2020-10-24 00:11:03 +0000 @@ -782,7 +782,7 @@ * * @return {@code true} on success. */ - /*public*/ boolean repositionByID(Long... joinIDs); + public boolean repositionByID(Long... joinIDs); /** * Reposition the cursor to the specified row in the result set. The row === modified file 'src/com/goldencode/p2j/persist/Persistence.java' --- src/com/goldencode/p2j/persist/Persistence.java 2020-09-20 08:07:24 +0000 +++ src/com/goldencode/p2j/persist/Persistence.java 2021-01-29 19:50:19 +0000 @@ -555,6 +555,9 @@ ** OM 20200919 Improved access to dmo metadata. ** ECF 20200919 Eliminate use of Session.refresh where no longer necessary. ** AIL 20200919 Initialize TemporaryDatabaseManager. +** ECF 20201026 Increase size of query LRU cache. +** OM 20201120 Avoid UniqueResultException reaching client UI. +** IAS 20201125 Do not re-use re-written queries */ /* @@ -616,6 +619,7 @@ import java.sql.*; import java.util.*; import java.util.Date; +import java.util.concurrent.*; import java.util.function.*; import java.util.logging.*; import com.goldencode.cache.*; @@ -1379,6 +1383,9 @@ * value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of * results at a time. Consult your JDBC driver documentation and/or source code to determine * whether this is an issue for your JDBC implementation. + * + * @param + * Type of object being listed. * * @param entities * Names of the DMO entities associated with this query statement. @@ -1395,6 +1402,8 @@ * an offset of 0 is used by default. * @param readOnlyQuery * {@code true} to execute the query in read-only mode, else {@code false}. + * @param noFlush + * No longer used. Should be removed. * * @return A list of results, or {@code null} if no result was found. * @@ -2557,6 +2566,9 @@ * * @return Merged DMO. * + * @throws PersistenceException + * in error + * * @deprecated */ public Record merge(Record dmo) @@ -2906,7 +2918,7 @@ // retrieve fully qualified name of LockManager implementation class and instantiate it String path = "database/" + database.getName() + "/p2j/lock_manager/class"; String className = Utils.getDirectoryNodeString(null, path, DEFAULT_LOCK_MANAGER, false); - LockManager manager = (LockManager) Class.forName(className).newInstance(); + LockManager manager = (LockManager) Class.forName(className).newInstance(); manager.setDatabase(database); // register lock table updater with lock manager if the former exists @@ -3609,14 +3621,9 @@ } } - if (exc != null) - { - throw new PersistenceException(msg, errnum, exc); - } - else - { - throw new PersistenceException(msg, errnum); - } + // NOTE: do not add the UniqueResultException as cause for the new exception because it will leak + // into UI (see ErrorManager.compileErrorEntries()) + throw new PersistenceException(msg, errnum); } /** @@ -3956,7 +3963,7 @@ class Context { /** Cache of FQL strings, with or without max results and start offsets, to queries. */ - private final ExpiryCache staticQueryCache = new LRUCache<>(100); + private final ExpiryCache staticQueryCache = new LRUCache<>(1000); /** Context-local buffer manager */ private final BufferManager bufferManager = BufferManager.get(); @@ -4094,12 +4101,19 @@ query = staticQueryCache.get(key); if (query == null) { - query = Session.createQuery(fql); - // NOTE: the FQL string is not converted to SQL at this time. The conversion - // is performed once, lazily, when the first navigation is required by a - // call of list(), scroll(), or uniqueResult() on the new query - - staticQueryCache.put(key, query); + String s = fql; + query = Session.createQuery(q -> + { + synchronized (staticQueryCache) + { + // NOTE: the FQL string is not converted to SQL at this time. The conversion + // is performed once, lazily, when the first navigation is required by a + // call of list(), scroll(), or uniqueResult() on the new query + + staticQueryCache.put(key, q); + } + }, + fql); } // update [maxResults] and [startOffset] because the cached query might have set === modified file 'src/com/goldencode/p2j/persist/PreselectQuery.java' --- src/com/goldencode/p2j/persist/PreselectQuery.java 2020-10-01 17:13:26 +0000 +++ src/com/goldencode/p2j/persist/PreselectQuery.java 2021-01-13 21:04:41 +0000 @@ -561,6 +561,9 @@ ** AIL 20200910 Restrict resetting offSet flag only for browsed queries. ** ECF 20200919 Persistence.load API signature change. ** OM 20201001 Dropped redundant ORDER BY elements in multi-table queries. +** OM 20201007 Optimized SortCriterion by using DmoMeta data instead of map lookups. +** _multiplex sort component matches the index direction. +** OM 20201120 Adjusted error messages. */ /* @@ -2980,7 +2983,8 @@ if (!repositionByID(serIDs, false)) { - ErrorManager.recordOrThrowError(7331, "Cannot reposition query to recid/rowid(s) given"); + ErrorManager.recordOrThrowError(7331, "", ""); + // "Cannot reposition query to recid/rowid(s) given". } } @@ -3018,9 +3022,8 @@ if (!repositionByID(serIDs, false)) { - ErrorManager.recordOrThrowError( - 7331, - "Cannot reposition query to recid/rowid(s) given"); + ErrorManager.recordOrThrowError(7331, "", ""); + // "Cannot reposition query to recid/rowid(s) given". } } @@ -3518,7 +3521,7 @@ */ public int getTableCount() { - return components.size(); + return components == null ? 0 : components.size(); } /** @@ -4829,7 +4832,7 @@ if (buffer == null) { throw new PersistenceException( - "Order by clause references an unrecognized buffer: " +alias + " (" + part + ")"); + "Order by clause references an unrecognized buffer: " + alias + " (" + part + ")"); } // Find or create the sub-list in which to store SortCriterion @@ -4841,14 +4844,8 @@ subSortMap.put(buffer, subSortCrit); } - // Create the SortCriterion instance and add it to the master list - // and to the appropriate sub-list. - -// String schema = buffer.getSchema(); - Class dmoIface = buffer.getDMOInterface(); - Class dmoClass = buffer.getDMOImplementationClass(); - - SortCriterion crit = new SortCriterion(buffer, part, /*schema,*/ dmoIface, dmoClass); + // create the SortCriterion instance and add it to the master list and to the appropriate sub-list + SortCriterion crit = new SortCriterion(buffer, part); sortCriteria.add(crit); subSortCrit.add(crit); @@ -4904,13 +4901,16 @@ if (multiplex) { - Class dmoIface = buffer.getDMOInterface(); - Class dmoClass = buffer.getDMOImplementationClass(); - String text = buffer.getDMOAlias() + "." + TemporaryBuffer.MULTIPLEX_FIELD_NAME + " asc"; + // the _multiplex is injected so that the query to benefit from the matching index + + Boolean ascIndex = IndexHelper.get(buffer.getDatabase()).getIndexDirection(buffer, sort); + StringBuilder sb = new StringBuilder(); + sb.append(buffer.getDMOAlias()).append(".").append(TemporaryBuffer.MULTIPLEX_FIELD_NAME); + sb.append(ascIndex == null || ascIndex ? " asc" : " desc"); try { - workList.add(new SortCriterion(buffer, text, dmoIface, dmoClass)); + workList.add(new SortCriterion(buffer, sb.toString())); } catch (PersistenceException exc) { === modified file 'src/com/goldencode/p2j/persist/PresortQuery.java' --- src/com/goldencode/p2j/persist/PresortQuery.java 2020-10-01 17:13:26 +0000 +++ src/com/goldencode/p2j/persist/PresortQuery.java 2020-10-09 21:13:00 +0000 @@ -89,6 +89,7 @@ ** 031 ECF 20181106 Added runtime support for {FIRST|LAST}-OF methods. ** 032 ECF 20200906 New ORM implementation. ** 033 OM 20201001 Dropped redundant ORDER BY elements in multi-table queries. +** OM 20201007 Optimized SortCriterion by using DmoMeta data instead of map lookups. */ /* @@ -1492,14 +1493,8 @@ while (iter.hasNext()) { RecordBuffer buffer = iter.next().getBuffer(); - Class dmoIface = buffer.getDMOInterface(); - Class dmoClass = buffer.getDMOImplementationClass(); - SortCriterion crit = new SortCriterion( - buffer, - buffer.getDMOAlias() + "." + DatabaseManager.PRIMARY_KEY + " asc", - dmoIface, - dmoClass); + buffer, buffer.getDMOAlias() + "." + DatabaseManager.PRIMARY_KEY + " asc"); sortCriteria.add(crit); } === modified file 'src/com/goldencode/p2j/persist/PropertyDefinition.java' --- src/com/goldencode/p2j/persist/PropertyDefinition.java 2020-09-24 17:41:02 +0000 +++ src/com/goldencode/p2j/persist/PropertyDefinition.java 2021-01-13 21:04:41 +0000 @@ -4,18 +4,20 @@ ** ** Copyright (c) 2013-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 CA 20130618 Created initial version. ** 002 ECF 20140623 Removed Hibernate dependency. This update simplifies the use of the type ** variable; this is a short-term workaround which breaks some result set -** metadata functionality (not currently in use), but it needs further -** analysis/work. +** metadata functionality (not currently in use), but it needs further analysis/work. ** 003 ECF 20140626 Simplified type check. ** 004 SVL 20140709 Added legacyName field. ** 005 CA 20190812 Added toString(). ** 006 IAS 20200908 Rework (de)serialization. ** 007 IAS 20200922 Get rid of possible NPE on serialization. +** SVL 20201030 Added format, label and columnLabel fields. +** OM 20201120 Added SCHEMA-MARSHAL implementation. */ + /* ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU Affero General Public License as @@ -71,13 +73,19 @@ package com.goldencode.p2j.persist; -import static com.goldencode.util.NativeTypeSerializer.*; import java.io.*; import java.util.*; +import static com.goldencode.p2j.persist.AbstractTempTable.*; +import static com.goldencode.util.NativeTypeSerializer.*; /** * Container for a DMO property's metadata: name, type and extent. Used to send the metadata of a * DMO to a remote side, when remote appserver calls are involved. + * + * TODO: add the other schema attributes of a property definition: help, initial, mandatory, validateMessage, + * validationExpression, description, decimals (for decimal type), caseSensitive (for character type) + * codePage, serializeName, xmlNodeName, xmlDataType, etc. + * All these will be serialized only if {@code schemaMarshalLevel == SM_FULL}. */ public class PropertyDefinition implements Externalizable @@ -99,8 +107,23 @@ /** The legacy name of this property. */ private String legacyName; - - static + + /** The format of this property. */ + private String format; + + /** The label of this property. */ + private String label; + + /** The column label of this property. */ + private String columnLabel; + + /** + * The SCHEMA-MARSHAL level for the parent temp-table. It is never {@code SM_DEFAULT} when object is to + * be serialized. Always {@code SM_DEFAULT} for a read object. + */ + private int schemaMarshalLevel = SM_DEFAULT; + + static { primitiveClasses.put("byte", byte.class); primitiveClasses.put("short", short.class); @@ -134,8 +157,7 @@ throws ClassNotFoundException { this.name = name; - Class cls = primitiveClasses.containsKey(type) ? primitiveClasses.get(type) - : Class.forName(type); + Class cls = primitiveClasses.containsKey(type) ? primitiveClasses.get(type) : Class.forName(type); verifyType(cls); this.type = cls; this.extent = NO_EXTENT; @@ -152,7 +174,7 @@ * The extent of this property. * * @throws ClassNotFoundException - * If the given type can not be resolved to a valid class. + * If the given {@code type} can not be resolved to a valid class. */ public PropertyDefinition(String name, String type, int extent) throws ClassNotFoundException @@ -186,11 +208,7 @@ */ public PropertyDefinition(String name, Class type, String legacyName) { - this.name = name; - verifyType(type); - this.type = type; - this.extent = NO_EXTENT; - this.legacyName = legacyName; + this(name, type, legacyName, null, null, null); } /** @@ -222,11 +240,80 @@ */ public PropertyDefinition(String name, Class type, int extent, String legacyName) { - this(name, type); + this(name, type, extent, legacyName, null, null, null); + } + + /** + * Create a new, no-extent, property definition. + * + * @param name + * The name of this property. + * @param type + * A class specifying the type of this property. + * @param legacyName + * The legacy name of this property. + * @param format + * The format of this property. + * @param label + * The label of this property. + * @param columnLabel + * The column label of this property. + */ + public PropertyDefinition(String name, + Class type, + String legacyName, + String format, + String label, + String columnLabel) + { + this(name, type, NO_EXTENT, legacyName, format, label, columnLabel); + } + + /** + * Create a new property definition. + * + * @param name + * The name of this property. + * @param type + * A class specifying the type of this property. + * @param legacyName + * The legacy name of this property. + * @param format + * The format of this property. + * @param label + * The label of this property. + * @param columnLabel + * The column label of this property. + */ + public PropertyDefinition(String name, + Class type, + int extent, + String legacyName, + String format, + String label, + String columnLabel) + { + this.name = name; + verifyType(type); + this.type = type; this.extent = extent; this.legacyName = legacyName; - } - + this.format = format; + this.label = label; + this.columnLabel = columnLabel; + } + + /** + * The SCHEMA-MARSHAL level to be used when serialization of the property. + * + * @param schemaMarshalLevel + * The configured SCHEMA-MARSHAL level. See static constants in {@link AbstractTempTable}. + */ + public void setSchemaMarshalLevel(int schemaMarshalLevel) + { + this.schemaMarshalLevel = schemaMarshalLevel; + } + /** * Get the type of this property. * @@ -236,7 +323,7 @@ { return type; } - + /** * Get the extent of this property. * @@ -278,6 +365,36 @@ } /** + * Get format of this property. + * + * @return format of this property. + */ + public String getFormat() + { + return format; + } + + /** + * Get label of this property. + * + * @return label of this property. + */ + public String getLabel() + { + return label; + } + + /** + * Get column label of this property. + * + * @return column label of this property. + */ + public String getColumnLabel() + { + return columnLabel; + } + + /** * Send the property definition to the specified output destination. * * @param out @@ -289,12 +406,15 @@ public final void writeExternal(ObjectOutput out) throws IOException { - writeString(out, name); - writeString(out, type == null ? null : type.getName()); - out.writeInt(extent); - writeString(out, legacyName); + writeString(out, (schemaMarshalLevel == SM_NONE) ? null : name); + writeString(out, (type == null || schemaMarshalLevel == SM_NONE) ? null : type.getName()); + out.writeInt((schemaMarshalLevel == SM_NONE) ? 0 : extent); + writeString(out, (schemaMarshalLevel == SM_NONE) ? null : legacyName); + writeString(out, (schemaMarshalLevel == SM_FULL) ? format : null); + writeString(out, (schemaMarshalLevel == SM_FULL) ? label : null); + writeString(out, (schemaMarshalLevel == SM_FULL) ? columnLabel : null); } - + /** * Read the property definition from the specified input source. * @@ -303,7 +423,9 @@ * * @throws IOException * In case of I/O errors. - * @throws ClassNotFoundException + * + * @throws ClassNotFoundException + * If the class of property could not be found/loaded. */ public final void readExternal(ObjectInput in) throws IOException, @@ -311,17 +433,20 @@ { name = readString(in); String tn = readString(in); - type = (Class)(tn == null ? null : Class.forName(tn)); + type = (tn == null) ? null : Class.forName(tn); extent = in.readInt(); legacyName = readString(in); + format = readString(in); + label = readString(in); + columnLabel = readString(in); } /** * Verify the given type is not null. - * + * * @param type * The type to check. - * + * * @throws NullPointerException * If type is null. */ @@ -341,12 +466,24 @@ @Override public String toString() { - String s = name + "[legacy: " + legacyName + "]: " + type.getSimpleName(); + StringBuilder sb = new StringBuilder(); + sb.append(name).append("[legacy: ").append(legacyName).append("]: ").append(type.getSimpleName()); if (extent != NO_EXTENT && extent != 0) { - s = s + "[" + extent + "]"; + sb.append("[").append(extent).append("]"); } - return s; + return sb.toString(); + } + + /** + * Configures the SCHEMA-MARSHAL level used for serialization. + * + * @param schemaMarshalLevel + * The new SCHEMA-MARSHAL level. See {@link AbstractTempTable} static constants for details. + */ + public void setMarshalLevel(int schemaMarshalLevel) + { + this.schemaMarshalLevel = schemaMarshalLevel; } } === modified file 'src/com/goldencode/p2j/persist/QueryWrapper.java' --- src/com/goldencode/p2j/persist/QueryWrapper.java 2020-09-18 09:44:42 +0000 +++ src/com/goldencode/p2j/persist/QueryWrapper.java 2021-01-21 22:49:45 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 ECF 20060430 @25885 Created initial version. Merged contents of ** AbstractQueryWrapper, CompoundQueryWrapper, ** PreselectQueryWrapper, and RandomAccessQuery- @@ -239,6 +239,8 @@ ** CA 20200918 INDEX-INFORMATION requires to interpret the 'execute' method for a dynamic query ** without evaluating the dynamic calls in the WHERE. ** Fixed multiple QUERY-OPEN() calls - must close the query before reopening. +** VVT 20210108 repositionByID() fixed to match 4gl behaviour: see #5038-39 +** OM 20201120 Added OFF-END callback. */ /* @@ -365,11 +367,14 @@ /** Default delegate to use before a real one is assigned */ private static final P2JQuery DEFAULT_DELEGATE = new DefaultDelegate(); + /** The {@code OFF-END} event string constant in upper case. */ + private static final String OFF_END_EVENT = "OFF-END"; + /** Delegate query */ private P2JQuery delegate = null; /** Reposition listeners registered with the delegate query */ - private WeakList repoListeners = new WeakList<>(); + private final WeakList repoListeners = new WeakList<>(); /** Should delegate be a scrolling query? */ private boolean scrolling = false; @@ -384,7 +389,7 @@ private boolean browsed = false; /** Opened listeners registered to get the message about query has been opened. */ - private List openedListeners = new ArrayList<>(); + private final List openedListeners = new ArrayList<>(); /** Flag indicating if the query is closing. */ private boolean closing = false; @@ -392,6 +397,9 @@ /** Flag indicating if the query was explicitly closed or is not open. */ private boolean closed = true; + /** Current off-end state. Used to detect when off-end state was encountered and fire OFF-END callback. */ + private boolean offEnd = false; + /** Flag indicating if the query has been advanced (via GET NEXT) to the next record. */ private boolean advanced = false; @@ -437,10 +445,13 @@ * dynamic query must appear exactly one time in FOR clause. */ private List substBuffers = null; - + /** External handler for create/delete result list entry methods. */ private ResultListHandler externalResultListHandler = null; + /** The only possible callback. {@code null} if not set.*/ + private CallbackData callback = null; + /** * Constructor which creates a static query and sets query scrolling mode for all delegate * queries assigned to this wrapper. @@ -702,6 +713,8 @@ { registerCleaner(); } + + offEnd = delegate._isOffEnd(); } /** @@ -1787,6 +1800,7 @@ advanced = true; handleQueryOffEnd(() -> getDelegate().first()); + maybeFireCallback(); } /** @@ -1810,6 +1824,7 @@ advanced = true; handleQueryOffEnd(() -> getDelegate().first(lockType)); + maybeFireCallback(); } /** @@ -1937,6 +1952,7 @@ } handleQueryOffEnd(() -> getDelegate().last()); + maybeFireCallback(); } /** @@ -1957,8 +1973,9 @@ { return; } - + handleQueryOffEnd(() -> getDelegate().last(lockType)); + maybeFireCallback(); } /** @@ -2092,6 +2109,7 @@ advanced = true; getDelegate().next(); + maybeFireCallback(); } /** @@ -2120,6 +2138,7 @@ advanced = true; getDelegate().next(lockType); + maybeFireCallback(); } /** @@ -2253,6 +2272,7 @@ } getDelegate().previous(); + maybeFireCallback(); } /** @@ -2279,6 +2299,7 @@ } getDelegate().previous(lockType); + maybeFireCallback(); } /** @@ -2406,6 +2427,7 @@ } getDelegate().current(); + maybeFireCallback(); } /** @@ -2427,6 +2449,7 @@ } getDelegate().current(lockType); + maybeFireCallback(); } /** @@ -2444,6 +2467,7 @@ public void unique() { getDelegate().unique(); + maybeFireCallback(); } /** @@ -2465,6 +2489,7 @@ public void unique(LockType lockType) { getDelegate().unique(lockType); + maybeFireCallback(); } /** @@ -2851,6 +2876,7 @@ } getDelegate().repositionByID(id); + maybeFireCallback(); } /** @@ -2868,6 +2894,9 @@ * right to coincide with the records being joined by the * underlying query. * + * If an array element is unknown, this element and all subsequent elements + * are ignored. + * * @throws ErrorConditionException * if this query is not a scrolling query; * if any of the specified IDs represents the unknown value. @@ -2878,8 +2907,20 @@ { return; } - + + for(int i = 0; i < joinIDs.length; i++) + { + if(joinIDs[i].isUnknown()) + { + rowid[] newIds = new rowid[i]; + System.arraycopy(joinIDs, 0, newIds, 0, i); + joinIDs = newIds; + break; + } + } + getDelegate().repositionByID(id1, joinIDs); + maybeFireCallback(); } /** @@ -2901,7 +2942,9 @@ @Override public boolean repositionByID(Long... joinIDs) { - return getDelegate().repositionByID(joinIDs); + boolean ret = getDelegate().repositionByID(joinIDs); + maybeFireCallback(); + return ret; } /** @@ -2924,8 +2967,9 @@ { return; } - + getDelegate().reposition(row); + maybeFireCallback(); } /** @@ -2952,6 +2996,7 @@ */ getDelegate().reposition(row); + maybeFireCallback(); } /** @@ -2980,7 +3025,9 @@ */ public boolean forward(NumberType rows) { - return getDelegate().forward(rows); + boolean ret = getDelegate().forward(rows); + maybeFireCallback(); + return ret; } /** @@ -3008,7 +3055,9 @@ */ public boolean forward(int rows) { - return getDelegate().forward(rows); + boolean ret = getDelegate().forward(rows); + maybeFireCallback(); + return ret; } /** @@ -3037,7 +3086,9 @@ */ public boolean backward(NumberType rows) { - return getDelegate().backward(rows); + boolean ret = getDelegate().backward(rows); + maybeFireCallback(); + return ret; } /** @@ -3065,7 +3116,9 @@ */ public boolean backward(int rows) { - return getDelegate().backward(rows); + boolean ret = getDelegate().backward(rows); + maybeFireCallback(); + return ret; } /** @@ -4354,12 +4407,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(); + + logical ret = getDelegate().getNext(); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4379,12 +4434,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lockType); + + logical ret = getDelegate().getNext(lockType); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4407,12 +4464,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock); + + logical ret = getDelegate().getNext(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4435,12 +4494,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock); + + logical ret = getDelegate().getNext(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4465,12 +4526,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock, nowait); + + logical ret = getDelegate().getNext(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4495,12 +4558,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock, nowait); + + logical ret = getDelegate().getNext(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4525,12 +4590,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock, nowait); + + logical ret = getDelegate().getNext(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-NEXT method (KW_GET_NEXT). * @@ -4555,12 +4622,14 @@ { return new logical(false); } - + advanced = true; - - return getDelegate().getNext(lock, nowait); + + logical ret = getDelegate().getNext(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4577,10 +4646,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(); + + logical ret = getDelegate().getPrevious(); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4603,10 +4674,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock); + + logical ret = getDelegate().getPrevious(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4629,10 +4702,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lockType); + + logical ret = getDelegate().getPrevious(lockType); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4655,10 +4730,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock); + + logical ret = getDelegate().getPrevious(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4683,10 +4760,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock, nowait); + + logical ret = getDelegate().getPrevious(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4711,10 +4790,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock, nowait); + + logical ret = getDelegate().getPrevious(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4739,10 +4820,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock, nowait); + + logical ret = getDelegate().getPrevious(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-PREV method (KW_GET_PREV). * @@ -4767,10 +4850,12 @@ { return new logical(false); } - - return getDelegate().getPrevious(lock, nowait); + + logical ret = getDelegate().getPrevious(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4787,12 +4872,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst()); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst()); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4815,12 +4902,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4843,12 +4932,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4871,12 +4962,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lockType)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lockType)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4901,12 +4994,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4931,12 +5026,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4961,12 +5058,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-FIRST method (KW_GET_1ST). * @@ -4991,12 +5090,14 @@ { return new logical(false); } - + advanced = true; - - return handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getFirst(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5013,10 +5114,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast()); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast()); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5039,10 +5142,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5065,10 +5170,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5091,10 +5198,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lockType)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lockType)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5119,10 +5228,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5147,10 +5258,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5175,10 +5288,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-LAST method (KW_GET_LAST). * @@ -5203,10 +5318,12 @@ { return new logical(false); } - - return handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + + logical ret = handleQueryOffEnd(() -> getDelegate().getLast(lock, nowait)); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5220,10 +5337,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(); + + logical ret = getDelegate().getCurrent(); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5240,10 +5359,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock); + + logical ret = getDelegate().getCurrent(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5260,10 +5381,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock); + + logical ret = getDelegate().getCurrent(lock); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5279,10 +5402,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lockType); + + logical ret = getDelegate().getCurrent(lockType); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5301,10 +5426,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock, nowait); + + logical ret = getDelegate().getCurrent(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5323,10 +5450,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock, nowait); + + logical ret = getDelegate().getCurrent(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5345,10 +5474,12 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock, nowait); + + logical ret = getDelegate().getCurrent(lock, nowait); + maybeFireCallback(); + return ret; } - + /** * Conversion of GET-CURRENT() method (KW_GET_CUR). * @@ -5367,8 +5498,10 @@ { return new logical(false); } - - return getDelegate().getCurrent(lock, nowait); + + logical ret = getDelegate().getCurrent(lock, nowait); + maybeFireCallback(); + return ret; } /** @@ -5523,6 +5656,208 @@ } /** + * Associates an internal procedure with an ABL callback event. + * + * @param eventName + * The eventName of the event. + * @param callback + * The eventName of the internal procedure associated with the callback event. + * @param context + * A handle to a procedure that contains the internal procedure specified by {@code callback}. If + * not specified, {@code THIS-PROCEDURE} is used as the procedure context. + * + * @return {@code true} when the method ends with success. + */ + @Override + public logical setCallbackProcedure(String eventName, String callback, handle context) + { + if (!isCallbackName(eventName)) + { + return new logical(false); + } + + if (getDataSet() == null) + { + ErrorManager.recordOrShowError(11951); + // SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset. + return new logical(false); + } + + if (callback != null) + { + if (context == null || context.isUnknown()) + { + // defaulting to THIS-PROCEDURE + context = ProcedureManager.thisProcedure(); + } + else + { + if (!context._isValid() || !(context.getResource() instanceof PersistentProcedure)) + { + ErrorManager.recordOrShowError(13273); + // Third argument to SET-CALLBACK must be a valid procedure handle or object reference. + return new logical(false); + } + } + + this.callback = new CallbackData(callback, context, null); + } + + return new logical(true); + } + + /** + * Configures a callback. Creates an association between a method within a class instance or an internal + * procedure within a persistent procedure, with an ABL callback event. + * + * @param callbackName + * The name of a callback. + * @param routineName + * The name of a method or an internal procedure to be associated. + * @param context + * The context in which the callback will be executed. + * + * @return {@code true} if {@code callbackName} is a correct callback name. The rest of parameters are + * ignored (note: manual states otherwise). + */ + @Override + public logical setCallback(String callbackName, String routineName, object context) + { + if (!isCallbackName(callbackName)) + { + return new logical(false); + } + + if (getDataSet() == null) + { + ErrorManager.recordOrShowError(11951); + // SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset. + return new logical(false); + } + + if (routineName != null) + { + CallbackData cbd; + if (context == null || context.isUnknown()) + { + // defaulting to THIS-OBJECT or to THIS-PROCEDURE, depending on the context + if (ObjectOps.thisObject().isUnknown()) + { + cbd = new CallbackData(routineName, ProcedureManager.thisProcedure(), null); + } + else + { + cbd = new CallbackData(routineName, null, ObjectOps.thisObject()); + } + } + else + { + cbd = new CallbackData(routineName, null, context); + } + + this.callback = cbd; + } + + return new logical(true); + } + + /** + * Applies a callback procedure, which allows execution of a defined event without duplicating the event + * procedure definition. + * + * @param eventName + * The event whose callback will be called. + * + * @return {@code true} if operation is successful. + */ + public logical applyCallback(String eventName) + { + DataSet ds = getDataSet(); + if (ds == null) + { + ErrorManager.recordOrShowError(11949); + // APPLY-CALLBACK is only for buffers that are members of datasets. + return new logical(false); + } + + if (!isCallbackName(eventName)) + { + return new logical(false); + } + + return new logical(invokeCallback(asHandle(), this.callback)); + } + + /** + * Retrieves the name of the internal procedure associated with the ABL callback for the specified event. + * + * @param eventName + * The name of the event. + * + * @return the name of the internal procedure associated with specified event. + */ + @Override + public character getCallbackProcName(String eventName) + { + if (getDataSet() == null) + { + if (buffers != null && buffers.size() > 0) + { + ErrorManager.recordOrShowError(12788, getName(), ((BufferImpl) buffers.get(0)).doGetName()); + // OFF-END CALLBACK not supported for query whose buffer is not in a dataset. + return new character(); + } + } + + if (!isCallbackName(eventName)) + { + return new character(); + } + + if (this.callback == null) + { + return new character(); + } + + return new character(this.callback.getTarget()); + } + + /** + * Retrieves the handle of the procedure that contains the internal procedure associated with the ABL + * callback for the specified event + * + * @param eventName + * The name of the event. + * + * @return a handle to the procedure that contains the callback procedure. + */ + @Override + public handle getCallbackProcContext(String eventName) + { + if (getDataSet() == null) + { + if (buffers != null && buffers.size() > 0) + { + ErrorManager.recordOrShowError(12788, getName(), ((BufferImpl) buffers.get(0)).doGetName()); + // OFF-END CALLBACK not supported for query whose buffer is not in a dataset. + return new handle(); + } + } + + if (!isCallbackName(eventName)) + { + return new handle(); + } + + if (this.callback == null || this.callback.getProcedure() == null) + { + return new handle(); + } + + return new handle(this.callback.getProcedure()); + } + + /** * Specifies the fields included in a record retrieval. * * @param dmo @@ -5551,6 +5886,22 @@ } /** + * Test whether the OFF-END event occurred and if so, fire the callback if one is defined. + */ + private void maybeFireCallback() + { + if (!offEnd && getDelegate()._isOffEnd()) + { + offEnd = true; + + if (getDataSet() != null && callback != null) + { + invokeCallback(asHandle(), this.callback); + } + } + } + + /** * Add the buffers associated with the given query to this QUERY's buffer list. *

* Will be a no-op when this is a {@link #dynamic} query. @@ -6228,6 +6579,103 @@ } /** + * Invokes a callback procedure or a callback method. The callback procedure/method must have an + * {@code INPUT} parameter of type {@code DATASET} or {@code DATASET-HANDLE}. The passed parameter is this + * {@code DataSet} object. + * + * @param cbd + * A {@code CallbackData} object holding the data necessary for invocation. May be {@code null}, + * in which case this method will silently return {@code yes} even though nothing is performed. + * @param self + * The object on which the event is applied (buffer or dataset). + * + * @return {@code true} if no errors were encountered during execution. + */ + protected boolean invokeCallback(handle self, CallbackData cbd) + { + if (cbd == null) + { + return true; // yep, nothing to do here + } + + try + { + ErrorManager.setSilent(false); + + SelfManager.pushSelf(self); + + // reference already assumes input-output, do not emit append + DataSetParameter dsp = new DataSetParameter(getDataSet(), false, ParameterOption.BY_REFERENCE); + if (cbd.getObject() != null) + { + // TODO: + // Method '' for callback must be valid public method. (13452) + + ObjectOps.invokeStandalone(cbd.getObject(), cbd.getTarget(), "I", dsp); + } + else if (cbd.getProcedure() != null && !cbd.getProcedure().isUnknown()) + { + ControlFlowOps.invokeInWithMode(cbd.getTarget(), cbd.getProcedure(), "I", dsp); + } + + // NOTE: sometimes the method return *NO* even if the callback was successfully called + return true; + } + catch (Exception e) + { + e.printStackTrace(); + return false; + } + finally + { + SelfManager.popSelf(); + + // Do not restore silent-mode! + } + } + + /** + * Obtains the {@code Dataset} of the first buffer of this query, if this is defined. + * + * @return the {@code Dataset} of the first buffer of this query, if this is defined. + */ + private DataSet getDataSet() + { + DataSet ds = null; + if (buffers != null && buffers.size() > 0) + { + BufferImpl buffer = (BufferImpl) buffers.get(0); + if (buffer.isAfterBuffer()) + { + ds = buffer._dataSet(); + } + } + return ds; + } + + /** + * Test whether a string is a valid CALLBACK name for a {@code DataSet Query}. This method is responsible + * for raising the 12352 error if the name specified is unknown or not {@code OFF-END} - the only supported + * event for dataset queries. + * + * @param someName + * The name to test. + * + * @return {@code true} only if {@code someName} is equals to "OFF-END". + */ + private static boolean isCallbackName(String someName) + { + if (!OFF_END_EVENT.equalsIgnoreCase(someName)) + { + ErrorManager.recordOrShowError(12352, someName != null ? someName : ""); + // Argument to query callback method must be a valid query event name such as OFF-END + return false; + } + + return true; + } + + /** * A no-op implementation of P2JQuery which is used as the * default delegate for the enclosing QueryWrapper. This * implementation behaves as a query which returns no results would, === modified file 'src/com/goldencode/p2j/persist/RandomAccessQuery.java' --- src/com/goldencode/p2j/persist/RandomAccessQuery.java 2020-09-24 17:51:18 +0000 +++ src/com/goldencode/p2j/persist/RandomAccessQuery.java 2021-01-13 21:04:41 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2004-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- --------------------------Description------------------------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 ECF 20051007 @23259 Created initial version. Backing query for ** converted queries which require a dynamic ** record retrieval semantic. @@ -328,6 +328,7 @@ ** 114 OM 20200915 Handled query navigation on fatal error cases. ** ECF 20200919 Persistence.load API signature change. ** CA 20200924 Replaced Method.invoke with ReflectASM. +** OM 20201120 Added javadoc note about a possible incorrect logged error. */ /* @@ -1254,8 +1255,7 @@ if (buffer.isReadonly()) { - // TODO: readonly buffers cannot be used in any FIND statement - // but can be used in CAN-FIND functions + // TODO: readonly buffers cannot be used in a FIND statement but can be used in CAN-FIND functions } buffer.initialize(); @@ -1300,7 +1300,9 @@ if (indexName == null && LOG.isLoggable(Level.SEVERE)) { - LOG.log(Level.SEVERE, String.format(ERROR_LOCATE_SORT_INDEX, sort)); + // NOTE: sometimes this is not a real warning: at least in findByRowid(), when a sort order is + // intentionally manufactured just to get [this] initialized, but index is not really used + LOG.log(Level.SEVERE, String.format(ERROR_LOCATE_SORT_INDEX, sort)); } } @@ -3380,7 +3382,7 @@ // - the query has an embedded CAN-FIND // - the query has a client-side where expression if (ffCache != null && - (activeBundleKey == FIRST ||activeBundleKey == LAST || activeBundleKey == UNIQUE) && + (activeBundleKey == FIRST || activeBundleKey == LAST || activeBundleKey == UNIQUE) && join == null && getExternalBuffers() == null) { key = FastFindCache.createKey(buffer, index, where, activeBundleKey, values); === modified file 'src/com/goldencode/p2j/persist/Record.java' --- src/com/goldencode/p2j/persist/Record.java 2020-09-18 11:12:02 +0000 +++ src/com/goldencode/p2j/persist/Record.java 2021-01-13 21:04:41 +0000 @@ -4,11 +4,12 @@ ** ** Copyright (c) 2019-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20191001 Created initial version with basic data and accessors. ** OM 20200110 Added support various data types and setter/getter signatures. ** 002 AIL 20200826 Handle getters should use a string to handle conversion without zero default ** for invalid resources. +** 003 OM 20201213 Added utility method used for debugging. */ /* @@ -1810,7 +1811,6 @@ */ protected void updateObjectRefCount(object oldVal, object newVal) { - } /** @@ -1824,39 +1824,68 @@ return _recordMeta(); } - //@Override - public String toString2() + /** + * Utility method used for debugging. + * + * @param oneLine + * Simplify the result to make it fit on a single line. Some details are skipped and column names. + * + * @return the description of the object as configured by parameter(s). + */ + public String toString2(boolean oneLine) { StringBuilder sb = new StringBuilder(); - sb.append("Record(").append(_recordMeta().legacyName).append(":").append(id).append("){\n"); - PropertyMeta[] pms = _recordMeta().getPropertyMeta(false); - for (int i = 0; i < pms.length; i++) + + RecordMeta meta = _recordMeta(); + if (oneLine) { - if (i != 0) - { - sb.append(",\n"); - } - sb.append('\t').append(pms[i].getLegacyName()).append(": "); - - int extent = pms[i].getExtent(); - if (extent == 0) - { + int last = data.length; + int start = (this instanceof TempTableRecord) ? 5 : 0; + sb.append(meta.legacyName).append('/').append(meta.tables[0]).append(':').append(id).append(" {"); + for (int i = start; i < last; i++) + { + if (i > 0) + { + sb.append(", "); + } + sb.append(data[i]); } - else + sb.append('}'); + } + else + { + sb.append("Record(").append(meta.legacyName).append(":").append(id).append("){\n"); + PropertyMeta[] pms = meta.getPropertyMeta(false); + for (int i = 0; i < pms.length; i++) { - for (int k = 0; k < extent; k++) + if (i != 0) { - sb.append(k == 0 ? "[" : ""); - sb.append(data[i + k]); + sb.append(",\n"); } + sb.append('\t').append(pms[i].getLegacyName()).append(": "); - i += extent - 1; - sb.append("]"); + int extent = pms[i].getExtent(); + if (extent == 0) + { + sb.append(data[i]); + } + else + { + for (int k = 0; k < extent; k++) + { + sb.append(k == 0 ? "[" : ""); + sb.append(data[i + k]); + } + + i += extent - 1; + sb.append("]"); + } } + + sb.append("\n}"); } - sb.append("\n}"); return sb.toString(); } } === modified file 'src/com/goldencode/p2j/persist/RecordBuffer.java' --- src/com/goldencode/p2j/persist/RecordBuffer.java 2020-10-07 16:04:19 +0000 +++ src/com/goldencode/p2j/persist/RecordBuffer.java 2021-01-29 00:53:41 +0000 @@ -2,7 +2,7 @@ ** Module : RecordBuffer.java ** Abstract : Encapsulates a data record and provides methods to operate on it ** -** Copyright (c) 2004-2020, Golden Code Development Corporation. +** Copyright (c) 2004-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 ECF 20050816 @22479 Created initial version. Abstract base class @@ -1079,7 +1079,7 @@ ** to validate() (conditional ValidationException throw) ** 320 AIL 20200611 Moved buffer manager's buffer loaded notify after record loading. ** 321 OM 20191101 Used new persistence API. -** 20200623 Added persistentProcedure member which stores the procedure who +** 322 OM 20200623 Added persistentProcedure member which stores the procedure who ** opened this buffer for the first time. ** IAS 20200819 Added table CRUD ops statistics support ** GES 20200731 Eliminated context-local lookups by using ErrorHelper. @@ -1109,6 +1109,16 @@ ** CA 20201007 Fixed buffer definitions from internal procedures/functions - the 'defining ** procedure' was incorrectly assumed as the caller, and not the currently executed ** external program. +** AIL 20201210 Added faster buffer-copy only for buffers which compatible explicit signatures. +** ECF 20201210 Reworked toString() to use legacy field names. +** AIL 20201223 Extended support for buffer-copy in fast mode. +** 20210104 Fixed assign trigger checking when doing fast buffer copy. +** 20210129 Setting active buffer before doing fast buffer copy. +** OM 20201120 Added DATA-SOURCE-MODIFIED and DECIMALS implementation. Made methods acessible to +** [serial] package. Fixed dataset events firing. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. +** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to +** local dmoMeta.legacyTable. */ /* @@ -1166,7 +1176,6 @@ package com.goldencode.p2j.persist; -import java.beans.*; import java.lang.InstantiationException; import java.lang.reflect.*; import java.sql.*; @@ -1175,7 +1184,6 @@ import java.util.logging.*; import com.goldencode.p2j.*; import com.goldencode.p2j.persist.orm.*; -import com.goldencode.p2j.persist.orm.Property; import com.goldencode.p2j.persist.orm.types.DataHandler; import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.dirty.*; @@ -1609,14 +1617,20 @@ private final boolean vst; /** - * Flags whether the record stored has been write-touched. That is, whether a setter was - * invoked on the DMO, regardless whether the value was not actually changed since the last - * load of the record. Unlike {@code recordChanged} that tracks external changes (from other - * users), this boolean value tracks write-touches only from current user. + * Flags whether the record stored has been write-touched. That is, whether a setter was invoked on the + * DMO, regardless whether the value was not actually changed since the last load of the record. Unlike + * {@code recordChanged} that tracks external changes (from other users), this boolean value tracks + * write-touches only from current user. */ private boolean isTouched = false; /** + * Flag used to avoid recursive call while invoking ROW-UPDATE event procedure while another block is + * finished. + */ + private boolean firingRowUpdate = false; + + /** * Collects all {@code Progress.Lang.Object} fields of the record. Only makes sense for temp * tables. When empty, the OO support (ref-counting) is disabled. */ @@ -1625,6 +1639,14 @@ /** Determines if this buffer is dynamic. */ private boolean dynamic = false; + /** + * Marker for a CREATE-ROW callback in process. When this flag is on, the buffer contains a transient + * record but the before image was not yet created for it. In the event of a DELETE operation in a + * subsequent callback, the record is simply discarded instead of creating a superfluous delete + * before-image for it. + */ + protected boolean createRowCallback = false; + /** The query off-end listener associated with this record buffer. */ private final QueryOffEndListener offEndListener = new QueryOffEndListener() { @@ -2284,9 +2306,9 @@ { String schema = DatabaseManager.getSchema(database); Class dmoClass = TableMapper.getDMOClass(schema, tableName); - Class dmoIface = DmoMetadataManager.getDMOInterface(dmoClass); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoClass); String ldbName = ConnectionManager.get().getLDBName(database); - dmo = createDynamicBufferForPermTable(dmoIface, actualBufName, ldbName); + dmo = createDynamicBufferForPermTable(dmoInfo.getAnnotatedInterface(), actualBufName, ldbName); } else { @@ -2741,8 +2763,7 @@ else { // permanent buffer - String tableName = TableMapper.getLegacyName((Buffer) srcBuffer.getDMOProxy()); - String actualBufName = bufName != null ? bufName.getValue() : tableName; + String actualBufName = bufName != null ? bufName.getValue() : srcBuffer.dmoInfo.legacyTable; dmo = createDynamicBufferForPermTable(srcBuffer.getDMOInterface(), actualBufName, @@ -3194,7 +3215,7 @@ DatabaseTriggerManager.handleError( 1003, DatabaseEventType.ASSIGN, - TableMapper.getLegacyName(dmoIface), + buffer.dmoInfo.legacyTable, TableMapper.getLegacyFieldName(dmoIface, prop)); } @@ -3903,6 +3924,17 @@ dstRec = dstBuf.getCurrentRecord(); } + boolean hasBefore = ((BufferImpl) srcProxy).isAfterBuffer() && + ((BufferImpl) dstProxy).isAfterBuffer() && + srcBuf.tableHandle().unwrapTempTable().isTrackingChanges().booleanValue(); + + // try a fast copy first + // if it worked, then we are done with the copy all together + if (fastCopy(srcBuf, dstBuf, srcFields != null && srcFields.length > 0, noLobs, validate, hasBefore)) + { + return; + } + Map srcLegacyFields = srcBuf.getLegacyFieldNameMap().getLegacyField2Name(); Map dstLegacyFields = dstBuf.getLegacyFieldNameMap().getLegacyField2Name(); @@ -3929,33 +3961,28 @@ OperationType.COPY); Map copyB4Map = null; - if (((BufferImpl) srcProxy).isAfterBuffer() && ((BufferImpl) dstProxy).isAfterBuffer()) + if (hasBefore) { - boolean before = srcBuf.tableHandle().unwrapTempTable().isTrackingChanges().booleanValue(); + BufferImpl srcB4Proxy = (BufferImpl) ((BufferImpl) srcProxy).beforeBuffer().getResource(); + BufferImpl dstB4Proxy = (BufferImpl) ((BufferImpl) dstProxy).beforeBuffer().getResource(); + RecordBuffer srcB4Buf = ((BufferReference) srcB4Proxy).buffer(); + RecordBuffer dstB4Buf = ((BufferReference) dstB4Proxy).buffer(); - if (before) - { - BufferImpl srcB4Proxy = (BufferImpl) ((BufferImpl) srcProxy).beforeBuffer().getResource(); - BufferImpl dstB4Proxy = (BufferImpl) ((BufferImpl) dstProxy).beforeBuffer().getResource(); - RecordBuffer srcB4Buf = ((BufferReference) srcB4Proxy).buffer(); - RecordBuffer dstB4Buf = ((BufferReference) dstB4Proxy).buffer(); - - copyB4Map = createSimplePropsMap(lFields, - srcB4Buf, - dstB4Buf, - srcB4Buf.getLegacyGetterMap(), - dstB4Buf.getLegacySetterMap(), - srcB4Buf.getExtentMap(), - dstB4Buf.getExtentMap(), - srcLegacyFields, - dstLegacyFields, - srcB4Buf.getPropsByGetter(), - dstB4Buf.getPropsBySetter(), - srcB4Buf.getGetterDatums(), - dstB4Buf.getSetterDatums(), - noLobs, - OperationType.COPY); - } + copyB4Map = createSimplePropsMap(lFields, + srcB4Buf, + dstB4Buf, + srcB4Buf.getLegacyGetterMap(), + dstB4Buf.getLegacySetterMap(), + srcB4Buf.getExtentMap(), + dstB4Buf.getExtentMap(), + srcLegacyFields, + dstLegacyFields, + srcB4Buf.getPropsByGetter(), + dstB4Buf.getPropsBySetter(), + srcB4Buf.getGetterDatums(), + dstB4Buf.getSetterDatums(), + noLobs, + OperationType.COPY); } copy(srcBuf, dstBuf, copyMap, copyB4Map, validate); @@ -4113,29 +4140,6 @@ } /** - * Copy the content of a DMO to another. Only matching fields are processed. Only the fields - * are copied, the special and surrogate data is not copied. - * - * @param dst - * The target DMO. - * @param src - * The source DMO. - * @param dstBuffer - * The destination buffer. Only used to get information on the legacy fields (their - * order and type). - * @param srcBuffer - * The source buffer. Only used to get information on the legacy fields (their - * order and type). - * - * @return {@code true} if operation is successful, and {@code false} on error (if the fields - * types are not matching). - */ - static boolean copyDMO(Record dst, Record src, BufferImpl dstBuffer, BufferImpl srcBuffer) - { - return BaseRecord.copy(src, dst); - } - - /** * BUFFER-COPY() handle-based method runtime. * * This method copies any common fields, determined by name, data type, and extent-matching, @@ -4180,8 +4184,23 @@ dstRec = dstBuf.getCurrentRecord(); } + BufferReference srcProxy = srcBuf.getDMOProxy(); + BufferReference dstProxy = dstBuf.getDMOProxy(); + boolean hasBefore = ((BufferImpl) srcProxy).isAfterBuffer() && + ((BufferImpl) dstProxy).isAfterBuffer() && + srcBuf.tableHandle().unwrapTempTable().isTrackingChanges().booleanValue(); + boolean hasPairs = !(pairs == null || pairs.isUnknown() || pairs.getValue().length() == 0); + boolean hasExcept = !(except == null || except.isUnknown() || except.getValue().length() == 0); + try { + // try a fast copy first + // if it worked, then we are done with the copy all together + if (fastCopy(srcBuf, dstBuf, hasPairs || hasExcept, noLobs.getValue(), true, hasBefore)) + { + return new logical(true); + } + Map dstSetters = dstBuf.getLegacySetterMap(); Map propsMap = getPropsMap(srcBuf, dstBuf, @@ -4196,32 +4215,25 @@ { Map propsB4Map = null; - BufferReference srcProxy = srcBuf.getDMOProxy(); - BufferReference dstProxy = dstBuf.getDMOProxy(); - if (((BufferImpl) srcProxy).isAfterBuffer() && ((BufferImpl) dstProxy).isAfterBuffer()) + if (hasBefore) { - boolean before = srcBuf.tableHandle().unwrapTempTable().isTrackingChanges().booleanValue(); + BufferImpl srcB4Proxy = (BufferImpl) ((BufferImpl) srcProxy).beforeBuffer().getResource(); + BufferImpl dstB4Proxy = (BufferImpl) ((BufferImpl) dstProxy).beforeBuffer().getResource(); + RecordBuffer srcB4Buf = (TemporaryBuffer) ((BufferReference) srcB4Proxy).buffer(); + RecordBuffer dstB4Buf = (TemporaryBuffer) ((BufferReference) dstB4Proxy).buffer(); + Map dstB4Setters = dstB4Buf.getLegacySetterMap(); - if (before) - { - BufferImpl srcB4Proxy = (BufferImpl) ((BufferImpl) srcProxy).beforeBuffer().getResource(); - BufferImpl dstB4Proxy = (BufferImpl) ((BufferImpl) dstProxy).beforeBuffer().getResource(); - RecordBuffer srcB4Buf = (TemporaryBuffer) ((BufferReference) srcB4Proxy).buffer(); - RecordBuffer dstB4Buf = (TemporaryBuffer) ((BufferReference) dstB4Proxy).buffer(); - Map dstB4Setters = dstB4Buf.getLegacySetterMap(); - - propsB4Map = getPropsMap(srcB4Buf, - dstB4Buf, - dstB4Setters, - dstB4Buf.getPropsBySetter(), - dstB4Buf.getSetterDatums(), - except, - pairs, - noLobs, - OperationType.COPY); - } + propsB4Map = getPropsMap(srcB4Buf, + dstB4Buf, + dstB4Setters, + dstB4Buf.getPropsBySetter(), + dstB4Buf.getSetterDatums(), + except, + pairs, + noLobs, + OperationType.COPY); } - + return copy(srcBuf, dstBuf, propsMap, propsB4Map, true); } } @@ -5521,6 +5533,184 @@ } /** + * Fast way to do the buffer-copy. This doesn't work in all scenarios, but can greatly fasten + * some cases (which are often in practice). This will check if the current scenario is trivial: + * no assign triggers, no before buffers, no explicit pair copy and explicit dmo signature match. + * For this case, the fields are set in bulk through direct access. + * + * @param srcBuf + * The source buffer from which the copy is done. + * @param dstBuf + * The destination buffer to which the copy is done. + * @param forcePair + * Flag to indicate if an explicit pair of fields should be copied / not copied. + * @param noLobs + * Flag to indicate if lobs should be omitted from the copy. + * @param validate + * {@code true} to validate the destination buffer after the copy. + * @param hasBefore + * Flag to indicate if the buffer have before buffer which should be also copied. + * + * @return {@code true} if the fast copy is eligible and could be done. + * {@code false} if the fast copy couldn't be applied for these buffers. + */ + private static boolean fastCopy(RecordBuffer srcBuf, + RecordBuffer dstBuf, + boolean forcePair, + boolean noLobs, + boolean validate, + boolean hasBefore) + { + Record srcRec = srcBuf.getCurrentRecord(); + Record dstRec = dstBuf.getCurrentRecord(); + DmoMeta srcMeta = srcBuf.getDmoInfo(); + DmoMeta dstMeta = dstBuf.getDmoInfo(); + DmoSignature srcSignature = srcMeta.getSignature(); + DmoSignature dstSignature = dstMeta.getSignature(); + + Iterator dstPropIt = dstBuf.getDmoInfo().getFields(false); + Set dstProps = new HashSet<>(); + while (dstPropIt.hasNext()) + { + Property prop = dstPropIt.next(); + dstProps.add(prop.name); + } + + if (!noLobs && !forcePair && !hasBefore && + !dstBuf.triggerTracker.hasAnyAssignTrigger(dstProps) && + DMOSignatureHelper.validBufferCopy(srcSignature, dstSignature)) + { + // all the field names are both in the source and the destination + // at this point the DMOs are similar enough to fasten the copy + boolean success = true; + + if (DMOSignatureHelper.exactPropertyOrder(srcSignature, dstSignature)) + { + // the fields are also in the same order, which means we can assign directly the values + // from the source to the destination + boolean batchError = true; + boolean error = false; + boolean inBatch = dstBuf.getBufferManager().isBatchAssignMode(); + + try + { + if (validate) + { + startBatch(true); + } + + dstRec.setActiveBuffer(dstBuf); + try + { + dstBuf.bulkCopy = true; + success = success && OrmUtils.setAllFields(srcRec, dstRec); + batchError = false; + } + finally + { + // make sure reset is called first as endBatch can throw an exception + dstRec.resetActiveState(); + + if (validate) + { + endBatch(batchError); + } + } + + if (success) + { + return true; + } + else + { + // otherwise, fallback to the classic implementation + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, + "Explicit signature checking is broken; unsuccessful fast buffer copy. " + + "Fallback to classic implementation."); + } + return false; + } + } + catch (ConditionException exc) + { + error = true; + throw exc; + } + catch (Exception exc) + { + error = true; + + DBUtils.handleException(srcBuf.getDatabase(), exc); + DBUtils.handleException(dstBuf.getDatabase(), exc); + + String msg = "Error performing buffer copy"; + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, msg, exc); + } + + // TODO: use proper error. + ErrorManager.recordOrThrowError(-1, msg); + return false; + } + finally + { + dstBuf.bulkCopy = false; + + if (dstBuf.isCommitPending() && !inBatch) + { + try + { + if (!error) + { + dstBuf.getPersistence().commit(dstBuf.getPersistenceContext()); + } + else + { + dstBuf.getPersistence().rollback(dstBuf.getPersistenceContext()); + } + } + catch (Exception exc) + { + DBUtils.handleException(srcBuf.getDatabase(), exc); + DBUtils.handleException(dstBuf.getDatabase(), exc); + + String msg = "Error " + + (error ? "rolling back" : "comitting") + + " during buffer copy"; + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, msg, exc); + } + } + finally + { + dstBuf.setCommitPending(false); + } + } + } + } + else + { + // TODO: implement a faster copy in case the signatures match but the field order + // doesn't match + if (LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, + "Unimplememted fast copy for explicit signature match, but different field order." + + "Fallback to classic implementation."); + } + return false; + } + } + + // we don't have proper conditions for fast-copy + return false; + } + + /** * Get the {@link #undoable} instance. * * @return See above. @@ -5713,7 +5903,7 @@ // at a sub-transaction boundary, validate and flush a new or changed record // Note: for a new record, this means default values will be saved in any fields which have not - // otherwise been explicitly updated + // otherwise been explicitly updated boolean global = registeredWithGlobalScope == 0; int blockDepth = bufferManager.getCurrentScope(); @@ -5780,6 +5970,13 @@ return; } + if (transaction && isTouched && !firingRowUpdate) + { + firingRowUpdate = true; + maybeFireRowUpdateEvent(); + isTouched = false; // prevent firing ROW-UPDATE a second time when record is released + firingRowUpdate = false; + } int blockDepth = bufferManager.getCurrentScope(); triggerTracker.commit(blockDepth); } @@ -6555,10 +6752,6 @@ { ErrorManager.recordOrThrowError(exc); } - finally - { - isTouched = false; - } } /** @@ -6649,6 +6842,17 @@ } /** + * Constructs and returns a string containing the P4GL schema definition of the table. The result is not + * cached so this method is to be used only for debug. + * + * @return a string containing the P4GL legacy schema definition of the table. + */ + public String tableDefinition() + { + return dmoInfo.getTableDefinition(); + } + + /** * Get the DMO proxy which references this buffer instance. * * @return DMO proxy object. @@ -6959,10 +7163,10 @@ */ protected String getFieldHelp(String property) { - return (String) getFieldValue(property, - fieldHelp, - RecordBuffer::getFieldHelp, - TableMapper::getLegacyFieldHelp); + return getFieldValue(property, + fieldHelp, + RecordBuffer::getFieldHelp, + TableMapper::getLegacyFieldHelp); } /** @@ -6975,10 +7179,10 @@ */ protected String getFieldColumnLabel(String property) { - return (String) getFieldValue(property, - fieldColumnLabels, - RecordBuffer::getFieldColumnLabel, - TableMapper::getLegacyFieldColumnLabel); + return getFieldValue(property, + fieldColumnLabels, + RecordBuffer::getFieldColumnLabel, + TableMapper::getLegacyFieldColumnLabel); } /** @@ -7040,10 +7244,10 @@ */ protected String getFieldLabel(String property) { - return (String) getFieldValue(property, - fieldLabels, - RecordBuffer::getFieldLabel, - TableMapper::getLegacyFieldLabel); + return getFieldValue(property, + fieldLabels, + RecordBuffer::getFieldLabel, + TableMapper::getLegacyFieldLabel); } /** @@ -7074,10 +7278,10 @@ */ protected String getFieldValidateExpression(String property) { - return (String) getFieldValue(property, - fieldValidateExpressions, - RecordBuffer::getFieldValidateExpression, - TableMapper::getLegacyFieldValidateExpression); + return getFieldValue(property, + fieldValidateExpressions, + RecordBuffer::getFieldValidateExpression, + TableMapper::getLegacyFieldValidateExpression); } /** @@ -7108,10 +7312,10 @@ */ protected String getFieldValidateMessage(String property) { - return (String) getFieldValue(property, - fieldValidateMessages, - RecordBuffer::getFieldValidateMessage, - TableMapper::getLegacyFieldValidateMessage); + return getFieldValue(property, + fieldValidateMessages, + RecordBuffer::getFieldValidateMessage, + TableMapper::getLegacyFieldValidateMessage); } /** @@ -7142,10 +7346,10 @@ */ protected String getFieldFormat(String property) { - return (String) getFieldValue(property, - fieldFormats, - RecordBuffer::getFieldFormat, - TableMapper::getLegacyFieldFormat); + return getFieldValue(property, + fieldFormats, + RecordBuffer::getFieldFormat, + TableMapper::getLegacyFieldFormat); } /** @@ -7176,10 +7380,10 @@ */ protected logical getFieldLiteralQuestion(String property) { - return (logical) getFieldValue(property, - fieldLiteralQuestions, - RecordBuffer::getFieldLiteralQuestion, - TableMapper::getLegacyFieldLiteralQuestion); + return getFieldValue(property, + fieldLiteralQuestions, + RecordBuffer::getFieldLiteralQuestion, + TableMapper::getLegacyFieldLiteralQuestion); } /** @@ -7207,7 +7411,6 @@ protected void rowState(Integer state) { // not a temp-table record - return; } /** @@ -7228,36 +7431,51 @@ * is the {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record. * * @param peer - * The new long rowid value of the peer record or {@code null}. + * The new rowid value of the peer record or {@code null}. */ protected void peerRowid(rowid peer) { // not a temp-table record - return; + } + + /** + * Sets the {@code origin-rowid} for AFTER-TABLE record. + * + * @param peer + * The new rowid value of the record or {@code null}. + */ + public void originRowid(rowid peer) + { + // not a temp-table record } /** * Gets {@code errorFlag} of this record. * - * @return Since this method is invoked for permanent records this will always return - * {@code unknown} value. + * @return the {@code errorFlag} of this record or {@code null} if it was not configured. If not null, + * the value is a bitwise combination of ERROR and REJECTED attribute. It's up to the called to + * extract the bit(s) it needs.
+ * Since this method is invoked for permanent records this will always return {@code unknown} + * value. */ - protected integer errorFlag() + protected Integer errorFlags() { // not a temp-table record - return new integer(); + return null; } /** - * Sets the {@code errorFlag} value this peer record. + * Sets or removes the {@code errorFlag} value this peer record. The {@code error} attribute of the record + * is composed of multiple bit flags. This method manages its value based on the parameters it receives. * - * @param err - * The new error flag. + * @param errFlag + * The error bit to be set or removed. + * @param set + * Use {@code true} to set the flag and {@code false} to remove it. */ - protected void errorFlag(integer err) + protected void updateErrorFlags(int errFlag, boolean set) { // not a temp-table record - return; } /** @@ -7281,7 +7499,6 @@ protected void errorString(Text string) { // not a temp-table record - return; } /** @@ -7334,26 +7551,26 @@ * buffer-specific value, it is returned. Then, for temp-tables, buffer-specific value of the * default buffer if returned. Then the value specified in schema is returned. * - * @param property - * Property name of the target buffer field. - * @param bufFieldVals - * Map of the buffer-specific attribute values for the fields of this buffer. - * @param defaultBufValGetter - * Function for getting buffer-specific attribute values for the fields of the default - * buffer (for temp tables only). - * @param schemaValGetter - * Function for getting attribute values defined in schema. + * @param property + * Property name of the target buffer field. + * @param bufFieldVals + * Map of the buffer-specific attribute values for the fields of this buffer. + * @param defaultBufValGetter + * Function for getting buffer-specific attribute values for the fields of the default + * buffer (for temp tables only). + * @param schemaValGetter + * Function for getting attribute values defined in schema. * * @return value of the target attribute for the given buffer field. */ - protected Object getFieldValue(String property, - Map bufFieldVals, + protected T getFieldValue(String property, + Map bufFieldVals, BiFunction defaultBufValGetter, BiFunction schemaValGetter) { if (bufFieldVals != null) { - Object res = bufFieldVals.get(property); + T res = bufFieldVals.get(property); if (res != null) { return res; @@ -7365,11 +7582,11 @@ RecordBuffer defaultBuffer = getDefaultBuffer(); if (!RecordBuffer.this.equals(defaultBuffer)) { - return defaultBufValGetter.apply(defaultBuffer, property); + return (T) defaultBufValGetter.apply(defaultBuffer, property); } } - return schemaValGetter.apply((Buffer) getDMOProxy(), property); + return (T) schemaValGetter.apply((Buffer) getDMOProxy(), property); } /** @@ -7699,7 +7916,7 @@ * * @return null. */ - protected Integer getMultiplexID() + public Integer getMultiplexID() { return null; } @@ -7870,8 +8087,7 @@ if (topEvent != null) { // cannot fire DELETE trigger while any other trigger is executing - String legacyTable = TableMapper.getLegacyName(dmoInterface); - DatabaseTriggerManager.handleError(3169, topEvent, legacyTable, null); + DatabaseTriggerManager.handleError(3169, topEvent, dmoInfo.legacyTable, null); } else { @@ -7994,11 +8210,11 @@ * parameters. *

* This method is invalid in base class so it will always throw an exception. It must be - * overwritten by {@link TemporaryBuffer#delete} with correct logic. + * overwritten by {@link TemporaryBuffer#delete} with correct logic. * * @param suppDMOs * The DMOs for the external (additional) buffers that are accessed in inner - * subselect, or {@code null} in case of a simple {@code where} predicate. + * subselect, or {@code null} in case of a simple {@code where} predicate. * @param where * An HQL where clause snippet which defines the restriction criteria to apply to the * delete. All references to properties in a DMO must be unqualified. @@ -8008,7 +8224,7 @@ * @throws PersistenceException * always. */ - protected void delete(DataModelObject[] suppDMOs, String where, Object[] args) + protected void delete(DataModelObject[] suppDMOs, String where, Object... args) throws PersistenceException { if (suppDMOs != null) @@ -8038,7 +8254,7 @@ * @throws PersistenceException * if an error occurs performing the bulk delete operation. */ - protected void delete(String where, Object[] args) + protected void delete(String where, Object... args) throws PersistenceException { release(true); @@ -8401,11 +8617,7 @@ */ protected int lookupNumFields() { - Class dmoIface = getDMOInterface(); - String schema = getSchema(); - String legacyName = TableMapper.getLegacyName(dmoIface); - - return TableMapper.getNumFields(schema, legacyName); + return TableMapper.getNumFields(getSchema(), dmoInfo.legacyTable); } /** @@ -8843,66 +9055,42 @@ return buf.toString(); } - String name = null; - try + if (metadata != null) { - Map getters = new LinkedHashMap<>(); + String name = null; + PropertyMeta[] propMeta = metadata.getPropertyMeta(false); + int len = propMeta.length; + String[] names = new String[len]; int maxWidth = 0; - Method[] methods = dmoClass.getDeclaredMethods(); - int len = methods.length; for (int i = 0; i < len; i++) { - Method next = methods[i]; - name = next.getName(); - boolean prefixGet = name.startsWith("get"); - boolean prefixIs = name.startsWith("is"); - if ((prefixGet || prefixIs) && - next.getParameterTypes().length == 0 && - !Collection.class.isAssignableFrom(next.getReturnType())) + PropertyMeta next = propMeta[i]; + name = next.getLegacyName(); + int extent = next.getExtent(); + if (extent > 0) { - name = name.substring(prefixGet ? 3 : 2); - name = Introspector.decapitalize(name); - getters.put(name, next); - maxWidth = Math.max(maxWidth, name.length()); + name = name + '[' + extent + ']'; } + names[i] = name; + maxWidth = Math.max(maxWidth, name.length()); } buf.append(" "); - StringHelper.leftAlignText(DatabaseManager.PRIMARY_KEY, maxWidth, buf); + StringHelper.leftAlignText(Session.PK, maxWidth, buf); buf.append(" : "); buf.append(record.primaryKey()); - for (Map.Entry entry : getters.entrySet()) + Object[] data = currentRecord.getData(this); + + for (int i = 0; i < len; i++) { - name = entry.getKey(); - Method next = entry.getValue(); + name = names[i]; buf.append(sep); buf.append(" "); StringHelper.leftAlignText(name, maxWidth, buf); buf.append(" : "); - try - { - Object value = next.invoke(record, (Object[]) null); - buf.append(formatProperty(value)); - } - catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) - { - // eat it - } - } - } - catch (Exception exc) - { - DBUtils.handleException(getDatabase(), exc); - - if (LOG.isLoggable(Level.WARNING)) - { - LOG.log(Level.WARNING, - describe() + ": error dumping record contents --> " + - getDMOName() + ":" + record.primaryKey() + " at property '" + - name + "'", - exc); + buf.append(formatProperty(data[i])); } } @@ -9226,7 +9414,7 @@ { if (currentRecord != null) { - setCurrentRecord(null, false, false, false, allowWriteTrigger); + setCurrentRecord(null, undo, false, false, allowWriteTrigger); } } @@ -9387,8 +9575,7 @@ { if (triggerTracker.isExecuting(det, recId)) { - String legacyTable = TableMapper.getLegacyName(dmoInterface); - DatabaseTriggerManager.handleError(2873, det, legacyTable, null); + DatabaseTriggerManager.handleError(2873, det, dmoInfo.legacyTable, null); } else { @@ -9677,7 +9864,7 @@ * * @return Persistence service object for this buffer's database. */ - protected Persistence getPersistence() + public Persistence getPersistence() { checkActive(); @@ -9897,13 +10084,43 @@ // the record is still transient. createScope = bufferManager.getCurrentScope(); + // invoke callback procedures first. Any trigger procedure is executed after this event is handled + if (isTemporary()) + { + if (buf._dataSet() != null) + { + // checks whether tracking is active + if (getParentTable().isTrackingChanges().toJavaType()) + { + try + { + createRowCallback = true; + if (!buf.invokeCallback(Buffer.ROW_CREATE_EVENT)) + { + BlockManager.returnError(); + } + } + finally + { + createRowCallback = false; + } + + if (currentRecord == null) + { + // the callback procedure/method deleted the newly created transient record + // nothing more to do, just return + return; + } + } + } + } + DatabaseEventType det = DatabaseEventType.CREATE; if (triggerTracker.isTriggerEnabled(det, null)) { if (triggerTracker.isExecuting(det, recId)) { - String legacyTable = TableMapper.getLegacyName(dmoInterface); - DatabaseTriggerManager.handleError(3168, det, legacyTable, null); + DatabaseTriggerManager.handleError(3168, det, dmoInfo.legacyTable, null); } else { @@ -9951,9 +10168,9 @@ // if this is buffer of an active AFTER-BUFFER, update the BEFORE-TABLE, too: BufferImpl after = (BufferImpl) dmoProxy; - if (!ignoreBeforeTracking && - after.isAfterBuffer() && - !after.dataSet().isUnknown() && + if (!ignoreBeforeTracking && + after.isAfterBuffer() && + after._dataSet() != null && TempTableBuilder.asTempTable((Temporary) after).isTrackingChanges().getValue()) { handle hBefore = after.beforeBuffer(); @@ -10020,7 +10237,7 @@ * @throws ErrorConditionException * if the buffer fails validation. */ - protected boolean validate(boolean throwValidationException) + public boolean validate(boolean throwValidationException) throws ValidationException { if (!isAvailable()) @@ -10071,8 +10288,7 @@ * @throws ValidationException * if the DMO state fails any unique or non-nullable constraint check. * @throws PersistenceException - * if there is any error gathering index metadata or a database - * error performing validation. + * if there is any error gathering index metadata or a database error performing validation. */ void validate(Record dmo) throws ValidationException, @@ -10573,6 +10789,30 @@ } /** + * Set the DMO implementation instance which backs this buffer with a another record, loaded from database. + * The buffer will help updating the new record, usually for internal, more complex processing. + *

+ * This is a dedicated method accessible only from a very specific places within the persistence package. + * When the new record is set, the one eventually existing is released. + *

+ * Note that the triggers are NOT fired when using this method. + * + * @param newCrtRecord + * DMO instance which represents the record currently backing this buffer. If {@code null}, the + * record (and possibly its corresponding lock) is released. + * + * @throws ErrorConditionException + * if there is an error transitioning a lock when releasing the current record; if there is an + * error validating or saving a transient record during flush. + * + * @see #setCurrentRecord(Record, boolean, boolean, boolean, boolean) + */ + void loadRecord(Record newCrtRecord) + { + setCurrentRecord(newCrtRecord, false, false, false, false); + } + + /** * Creates a new {@link Validation} instance using the passed arguments, and calls * {@link Validation#validateMaybeFlush()}. Depending on the {@link Validation#wasFlushed()} * state, it will {@link #reportChange report} the change so that listeners will be notified. @@ -11267,6 +11507,12 @@ boolean dirtyCopy, boolean allowWriteTrigger) { + if (dmoProxy != null) + { + // reset the DATA-SOURCE-MODIFIED + ((Buffer) dmoProxy).setDataSourceModified(false); + } + if (newCrtRecord != null) { getTxHelper().checkTransaction(persistence.getDatabase().getId()); @@ -11306,6 +11552,13 @@ } } + // the ROW-UPDATE event is fired "immediately before the record is updated in the temp-tables" + // NOTE: because the way FWD handles transactions that moment is difficult to intercept. When a change + // operation occurred on the record, our validation has a secondary effect of flushing the + // record to database so at this moment the record is already updated and its flags set to CACHED + // and CHANGED flag is off. To detect if the record was indeed changed we test [isTouched] flag + maybeFireRowUpdateEvent(); + boolean clearValidationState = true; try { @@ -11388,8 +11641,7 @@ if (this.currentRecord != null) { - // store placeholder record if releasing current record and we - // haven't done so previously + // store placeholder record if releasing current record and we haven't done so previously if (snapshot == null && (newCrtRecord == null || newlyCreated)) { if (undoable != null) @@ -11423,7 +11675,7 @@ else { // try to release the lock - RecordIdentifier ident = this.currentRecord.getRecordLockIdentifier(); + RecordIdentifier ident = this.currentRecord.getRecordLockIdentifier(); lockContext.lock(ident, LockType.NONE, 0L, this); } } @@ -11464,9 +11716,7 @@ { if (LOG.isLoggable(Level.WARNING)) { - LOG.log(Level.WARNING, - "Error detaching record from session: " + toString(), - exc); + LOG.log(Level.WARNING, "Error detaching record from session: " + toString(), exc); } ErrorManager.recordOrThrowError(exc); @@ -11480,6 +11730,7 @@ { this.undoable.checkUndoable(true); } + this.currentRecord = newCrtRecord; this.recordChanged = false; // invalidate CURRENT-CHANGED flag (see above) this.isTouched = false; @@ -11521,6 +11772,27 @@ } /** + * Checks whether the conditions for firing ROW-UPDATE event are met. If affirmative the event is triggered + * and the dataset's callback is invoked with a handle to this buffer as parameter. + */ + private void maybeFireRowUpdateEvent() + { + if (isTouched && isTemporary() && this.currentRecord != null) + { + BufferImpl buf = (BufferImpl) dmoProxy; + if (buf._dataSet() != null) + { + // checks whether tracking is active + if (getParentTable().isTrackingChanges().toJavaType()) + { + buf.invokeCallback(Buffer.ROW_UPDATE_EVENT); + ErrorManager.clearPending(); // if an error is raised in this callback it is simply ignored + } + } + } + } + + /** * Store the undoability of this buffer in the record's state. Even though the same record * may be stored in multiple buffers at the same time, this is safe because (1) a record * instance is unique to a user session, and (2) buffers backed by the same table must have @@ -11785,7 +12057,7 @@ } // TODO: use something more reliable (but efficient) here to determine whether this is a setter - isSetter = Void.TYPE.equals(method.getReturnType()); + isSetter = (method.getReturnType() == Void.TYPE); // if another buffer created the current record, delegate setter method calls to it, // to ensure all validation and flushing is tracked properly @@ -11873,6 +12145,7 @@ } } + boolean denormalized = false; if (isSetter) { pm = propsBySetter.get(method); @@ -11889,6 +12162,7 @@ { int extIdx = (int) args[0]; // already computed as 0-base pm = metadata.getPropertyMeta(false)[pm.getOffset() + extIdx]; + denormalized = true; } property = pm.getName(); @@ -11955,9 +12229,9 @@ // has a BEFORE-TABLE to keep the changes in and tracking is activated (not in // FILL mode) BufferImpl after = (BufferImpl) RecordBuffer.this.dmoProxy; - if (!ignoreBeforeTracking && - after.isAfterBuffer() && - !after.dataSet().isUnknown() && + if (!ignoreBeforeTracking && + after.isAfterBuffer() && + after._dataSet() != null && TempTableBuilder.asTempTable((Temporary) after).isTrackingChanges().getValue()) { handle hBefore = after.beforeBuffer(); @@ -11979,6 +12253,7 @@ // keep the reference to the datasource-rowid, ((TemporaryBuffer) before.buffer()).getCurrentRecord()._originRowid( ((TemporaryBuffer) after.buffer()).getCurrentRecord()._originRowid()); + before.buffer().flush(); before.setUpBeforeBuffer(false); // do not release the before buffer, let it hold the newly created row } @@ -11996,6 +12271,8 @@ if (isSetter) { + isTouched = true; + // share dirty if needed if (currentRecord!= null && isDirtyRead && dirtyContext != null && !isTemporary()) { @@ -12046,7 +12323,10 @@ changed = diffs != null; if (changed) { - addUnreportedChanges(ormIndex, extent ? (Integer) args[0] : 0, diffs); + // NOTE: if the property was denormalized, the property index [ormIndex] already contains + // the extent index + int extentIndex = extent && !denormalized ? (Integer) args[0] : 0; + addUnreportedChanges(ormIndex, extentIndex, diffs); } } @@ -12071,7 +12351,7 @@ DatabaseTriggerManager.handleError( 1003, DatabaseEventType.ASSIGN, - TableMapper.getLegacyName(dmoIface), + dmoInfo.legacyTable, TableMapper.getLegacyFieldName(dmoIface, property)); } } @@ -13032,25 +13312,6 @@ } /** - * A Reversible version which affects more than one record. - */ - protected abstract class AbstractBulkReversible - extends Reversible - { - /** - * This method checks if this bulk reversible touched the record with - * the given key. - * - * @param key - * The record to be checked. - * - * @return true if the record was touched by this bulk - * reversible. - */ - protected abstract boolean containsRecord(Long key); - } - - /** * A lightweight implementation of the Undoable interface * which restores to the record buffer the current record and snapshot * which were active at the time of backup. @@ -13268,12 +13529,12 @@ * @param propertyName * Property name. * @param extentIndex - * Index of an element in an extent property or null. If present, the index should + * Index of an element in an extent property or {@code null}. If present, the index should * be zero-based. * @param accessor * The cached accessor for this instance. * @param extent - * The extent value of this property or null. + * The extent value of this property or {@code null}. * @param propertyMeta * The cached property meta. * @param indexed @@ -13351,15 +13612,21 @@ DatumAccess that = (DatumAccess) o; - if (extentIndex != null ? !extentIndex.equals(that.extentIndex) - : that.extentIndex != null) - { + if ((this.extentIndex == null) != (that.extentIndex == null)) + { + // both must have or both must not have extent + return false; + } + + if (this.extentIndex != null && this.extentIndex != that.extentIndex) + { + // if both have but it is different return false; } return propertyName.equals(that.propertyName); } - + /** * Get the {@link #propertyMeta}. * @@ -13389,7 +13656,7 @@ { return accessor; } - + /** * Get field name. * @@ -13410,6 +13677,19 @@ { return extentIndex; } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("DatumAccess{"); + sb.append(propertyName).append("/").append(propertyMeta.getLegacyName()); + if (extent != null && extent > 0) + { + sb.append("[").append(extent).append("]"); + } + sb.append("}"); + return sb.toString(); + } } /** @@ -13650,6 +13930,171 @@ // should be unreachable return null; } - + } + + /** + * Obtain a string representation of the table content stored on SQL. This is a SQL-wise view of the + * primary table of the DMO, not an actual ABL data. Only to be used in debug purposes.
+ * Notes: + *

    + *
  • the EXTENT properties are not shown unless the table is denormalized (in which case the these + * properties are stored in the primary table); + *
  • the name of the columns in the returned string are the SQL column names, not the legacy names; + *
  • the hidden, reserved before/after-table specific attributes are visible for temp-tables; + *
  • the larger string/character values are cut to max 32 chars; + *
+ * + * @param limit + * The maximum number of rows to be retrieved. Use 0 or negative to get them all. + * + * @return A string with the table content in a tabular format. + * + * @throws PersistenceException + * @throws SQLException + * If any issue was encountered. Since this method is supposed to be called only in debug mode, + * it's up to programmer to decide what went wrong. + */ + public String sqlTableContent(int limit) + throws PersistenceException, SQLException + { + String sql = "select * from " + dmoInfo.sqlTable; + if (limit > 0) + { + sql += " limit " + limit; + } + + ResultSet res = persistence.getSession().getConnection().prepareStatement(sql).executeQuery(); + ResultSetMetaData md = res.getMetaData(); + int cc = md.getColumnCount(); + int[] lens = new int[cc]; + + List rows = new ArrayList<>(10); + while (res.next()) + { + String[] row = new String[cc]; + for (int k = 1; k <= cc; k++) + { + row[k - 1] = res.getString(k); + int length = row[k - 1] == null ? 4 : row[k - 1].length(); + if (lens[k - 1] < length) + { + lens[k - 1] = length; + } + } + rows.add(row); + } + + StringBuilder sb = new StringBuilder(); + sb.append("ABL: ").append(dmoInfo.legacyTable).append("/ SQL:").append(dmoInfo.sqlTable).append("\n"); + for (int k = 1; k <= cc; k++) + { + String label = md.getColumnLabel(k); + if (lens[k - 1] < label.length()) + { + lens[k - 1] = label.length(); + } + + if (lens[k - 1] > 32) + { + lens[k - 1] = 32; + } + else if (lens[k - 1] < 4) + { + lens[k - 1] = 4; + } + + if (k != 1) + { + sb.append(" | "); + } + fit(sb, label, lens[k - 1], md.getColumnType(k)); + } + sb.append("\n"); + + for (int k = 1; k <= cc; k++) + { + if (k != 1) + { + sb.append("-+-"); + } + int len = lens[k - 1]; + while (len-- > 0) + { + sb.append("-"); + } + } + sb.append("\n"); + + for (String[] row : rows) + { + for (int k = 1; k <= cc; k++) + { + if (k != 1) + { + sb.append(" | "); + } + fit(sb, row[k - 1], lens[k - 1], md.getColumnType(k)); + } + + sb.append("\n"); + } + + res.close(); + return sb.toString(); + } + + /** + * Helper method for {@link #sqlTableContent(int)}. Used for generating the tabular pretty format. + * + * @param sb {@code StringBuilder} to put the result into. + * The + * @param text + * The value to be formatted. + * @param size + * The available space. + * @param type + * The column type. Used for alignment. + */ + private void fit(StringBuilder sb, String text, int size, int type) + { + boolean leftAlign = false; + switch (type) + { + case Types.NCHAR: + case Types.NVARCHAR: + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + leftAlign = true; + break; + } + + String ret; + if (text == null) + { + ret = "null"; + } + else if (text.length() <= size) + { + ret = text; + } + else + { + ret = text.substring(0, size); + } + + int k = size - ret.length(); + if (leftAlign) + { + sb.append(ret); + } + while (k-- > 0) + { + sb.append(" "); + } + if (!leftAlign) + { + sb.append(ret); + } } } === removed file 'src/com/goldencode/p2j/persist/Reversible.java' --- src/com/goldencode/p2j/persist/Reversible.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/Reversible.java 1970-01-01 00:00:00 +0000 @@ -1,254 +0,0 @@ -/* -** Module : Reversible.java -** Abstract : Abstract base class for reversible database actions -** -** Copyright (c) 2004-2020, Golden Code Development Corporation. -** -** -#- -I- --Date-- --JPRM-- ---------------------------Description------------------------------ -** 001 ECF 20070130 @32042 Created initial version. Moved implementation -** from inner class of RecordBuffer to a top -** level class. -** 002 ECF 20071221 @36547 Modified constructor. Throw NPE if dmo -** parameter is null. -** 003 CA 20080604 @38538 Allow the DMO to be changed (when a DELETE or -** CREATE action is passed to the parent block, -** the DMO must be set to the chain's DMO). -** Added new c'tor variant. -** 004 CA 20080606 @38598 Added duplicate() method. -** 005 ECF 20080628 @39071 Track whether DMO was newly created. -** 006 ECF 20080925 @39944 Refactored to reduce memory use. Some -** functionality moved to AbstractReversible and -** concrete subclasses. -** 007 ECF 20081020 @40210 Added methods necessary to fix rollback -** problems at the full transaction level. -** 008 CA 20091202 @44468 Removed the newlyCreated c'tor parameter - the -** flag will be set on rollback, depending on the -** existence of Create operations; currently is set -** only for undoable tables. -** 009 ECF 20140305 Replaced Apache commons logging with J2SE logging. -** 010 ECF 20200906 New ORM implementation. -*/ - -/* -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU Affero General Public License as -** published by the Free Software Foundation, either version 3 of the -** License, or (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU Affero General Public License for more details. -** -** You may find a copy of the GNU Affero GPL version 3 at the following -** location: https://www.gnu.org/licenses/agpl-3.0.en.html -** -** Additional terms under GNU Affero GPL version 3 section 7: -** -** Under Section 7 of the GNU Affero GPL version 3, the following additional -** terms apply to the works covered under the License. These additional terms -** are non-permissive additional terms allowed under Section 7 of the GNU -** Affero GPL version 3 and may not be removed by you. -** -** 0. Attribution Requirement. -** -** You must preserve all legal notices or author attributions in the covered -** work or Appropriate Legal Notices displayed by works containing the covered -** work. You may not remove from the covered work any author or developer -** credit already included within the covered work. -** -** 1. No License To Use Trademarks. -** -** This license does not grant any license or rights to use the trademarks -** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks -** of Golden Code Development Corporation. You are not authorized to use the -** name Golden Code, FWD, or the names of any author or contributor, for -** publicity purposes without written authorization. -** -** 2. No Misrepresentation of Affiliation. -** -** You may not represent yourself as Golden Code Development Corporation or FWD. -** -** You may not represent yourself for publicity purposes as associated with -** Golden Code Development Corporation, FWD, or any author or contributor to -** the covered work, without written authorization. -** -** 3. No Misrepresentation of Source or Origin. -** -** You may not represent the covered work as solely your work. All modified -** versions of the covered work must be marked in a reasonable way to make it -** clear that the modified work is not originating from Golden Code Development -** Corporation or FWD. All modified versions must contain the notices of -** attribution required in this license. -*/ - -package com.goldencode.p2j.persist; - -import java.util.logging.*; -import com.goldencode.p2j.util.LogHelper; - -/** - * Abstract base class of various types of database actions that must be - * reversible. Invocation of the {@link #rollback(boolean, boolean)} method - * reverses a specific database action on that record. The action taken to - * roll back the original action depends upon the subclass' implementation of - * the rollback method. - */ -abstract class Reversible -{ - /** Logger */ - protected static final Logger LOG = LogHelper.getLogger(Reversible.class); - - /** Was DMO record newly created at the time Reversible was created? */ - private boolean newlyCreated; - - /** - * Get a string representation of the internal state of this object, - * primarily for debugging purposes. - * - * @return String representing this object's state. - */ - public String toString() - { - return getAction() + " of " + getDMOName() + " [ID " + getId() + "]"; - } - - /** - * Get a string which briefly describes the action which this object - * will reverse upon rollback. - * - * @return Action string. - */ - protected String getAction() - { - return null; - } - - /** - * Indicate whether this Reversible implementation stores its - * DMO locally, as opposed to storing an identifier and having to retrieve - * the DMO from the backing database, or not making a DMO available at all. - * - * @return true if the DMO is stored locally, else - * false. - */ - protected abstract boolean storesDMO(); - - /** - * Get the unqualified interface name of the DMO being managed by this - * reversible. - * - * @return DMO interface name. - */ - protected abstract String getDMOName(); - - /** - * Roll back the database change represented by this object. - *

- * Subclasses must implement this method in a manner appropriate to - * the database actions they represent. - * - * @param rollForward - * If true, this method is being invoked in order to - * re-roll forward a non-undoable change, after a database-level - * rollback. This indicates the modification is made within a - * new transaction invoked after the database-level rollback. - * If false, this method is being invoked in order to - * roll back an undoable change within the context of an - * application-level, full transaction or sub-transaction - * rollback. - * @param transaction - * true if this rollback occurs at a full transaction - * boundary, else false. - * - * @throws PersistenceException - * if an error occurs at the database or in the persistence - * layer when reversing the action. - */ - protected abstract void rollback(boolean rollForward, boolean transaction) - throws PersistenceException; - - /** - * Get the underlying DMO. - * - * @return DMO. - */ - protected abstract Record getDMO(); - - /** - * Set the underlying DMO for this reversible action. - * - * @param dmo - * The underlying DMO; may not be null. - * - * @throws NullPointerException - * if dmo is null. - */ - protected abstract void setDMO(Record dmo); - - /** - * Get the ID of the underlying DMO. - * - * @return DMO ID. - */ - protected abstract Long getId(); - - /** - * Prepare for this reversible to be rolled back. Generally, this will - * involve readying the DMO which will be acted upon by the rollback - * actions. - * - * @param dmo - * If non-null, the caller is providing a DMO which - * it thinks is the appropriate object to act upon during rollback - * processing. - */ - protected void prepareRollback(Record dmo) - { - } - - /** - * Indicate whether the DMO which is the target of a rollback was newly - * created by the current context. - * - * @return true to indicate target DMO was newly created by - * the current context; else false. - */ - protected boolean isNewlyCreated() - { - return newlyCreated; - } - - /** - * Indicate whether the DMO which is the target of a rollback was newly - * created by the current context. - * - * @param isNew - * true to indicate dmo is newly created - * by the current context; else false. - */ - protected void setNewlyCreated(boolean isNew) - { - this.newlyCreated = isNew; - } - - /** - * Duplicate this reversible action. Implementations which require - * this behavior must override this method. - * - * @param key - * The key to set for the new reversible action. - * - * @return This implementation does not return normally; override to - * return a duplicate of this reversible. - * - * @throws UnsupportedOperationException - * If this operation is not supported. - */ - Reversible duplicate(Long key) - { - throw new UnsupportedOperationException( - "Duplicate is not supported for reversible " + - this.toString()); - } -} === added file 'src/com/goldencode/p2j/persist/SerializeHiddenable.java' --- src/com/goldencode/p2j/persist/SerializeHiddenable.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/SerializeHiddenable.java 2020-11-18 21:54:53 +0000 @@ -0,0 +1,102 @@ +/* +** Module : SerializeHiddenable.java +** Abstract : Declares the public accessors of SERIALIZE-HIDDEN attribute of DataSet/BufferField. +** +** Copyright (c) 2020, Golden Code Development Corporation. +** +** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** 001 OM 20190403 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist; + +import com.goldencode.p2j.util.*; + +/** + * Declares the public accessors of SERIALIZE-HIDDEN attribute of {@code DataSet}/ {@code BufferField}. + */ +public interface SerializeHiddenable +{ + /** + * Checks whether fields are written when the temp-table is serialized, usually as JSON or XML. + * This represents the getter for {@code SERIALIZE-HIDDEN} ABL attribute. + * + * @return {@code true} when the fields are skipped from serialization. + */ + @LegacyAttribute(name = "SERIALIZE-HIDDEN") + public logical getSerializeHidden(); + + /** + * Allow to configure whether fields are written when the temp-table is serialized, usually as JSON + * or XML. This represents the setter for {@code SERIALIZE-HIDDEN} ABL attribute. + * + * @param val + * {@code true} when the fields are to be skipped from serialization. + */ + @LegacyAttribute(name = "SERIALIZE-HIDDEN", setter = true) + public void setSerializeHidden(logical val); + + /** + * Allow to configure whether the filed is written when the temp-table is serialized, usually as JSON + * or XML. This represents the setter for {@code SERIALIZE-HIDDEN} ABL attribute. + * + * @param val + * {@code true} when the fields are to be skipped from serialization. + */ + @LegacyAttribute(name = "SERIALIZE-HIDDEN", setter = true) + public void setSerializeHidden(boolean val); +} + === modified file 'src/com/goldencode/p2j/persist/SortCriterion.java' --- src/com/goldencode/p2j/persist/SortCriterion.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/SortCriterion.java 2020-10-09 21:13:00 +0000 @@ -90,6 +90,8 @@ ** 035 ECF 20200906 New ORM implementation. ** 036 OM 20201001 Dropped redundant ORDER BY elements in multi-table queries. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** OM 20201007 Optimized SortCriterion by using DmoMeta data instead of map lookups. Simplified +** API by dropping unneeded parameters. Optimized sort phrase parsing. */ /* @@ -211,6 +213,7 @@ /** Flag indicating if the 'withNulls' is activated in {@link #toWhereExpression}. */ private static final boolean WHERE_WITH_NULLS = "true".equalsIgnoreCase(System.getProperty("WHERE_WITH_NULLS")); + /** Shared cache of immutable {@code SortCriterion} lists */ private static final ExpiryCache> cache = new LRUCache<>(65536); @@ -242,7 +245,7 @@ private final String propertyName; /** Sort property name, including subscript if any, usually qualified */ - private String originalName; + private final String originalName; /** Sort property name, usually qualified */ private String name; @@ -253,9 +256,6 @@ /** SQL dialect which used this sort criterion; used for rtrim calls on character properties */ private Dialect dialect = null; - /** The target database. */ - private final Database database; - /** * Constructor which parses original sort component text, deriving the property name, sort * direction, getter method for the property, case sensitivity of the property (character type @@ -271,22 +271,15 @@ * Record buffer. * @param text * Text representation of sort component, as provided by query. - * @param dmoIface - * DMO interface. - * @param dmoClass - * DMO implementation class. * * @throws PersistenceException * if there is any error with the text representation of the sort * component provided by the caller. */ - SortCriterion(RecordBuffer buffer, - String text, - Class dmoIface, - Class dmoClass) + SortCriterion(RecordBuffer buffer, String text) throws PersistenceException { - this(buffer.getDialect(), buffer.getDatabase(), text, dmoIface, dmoClass, false); + this(buffer.getDialect(), text, buffer.dmoInfo, false); } /** @@ -304,27 +297,17 @@ * * @param dialect * Database dialect. - * @param database - * The target database. * @param text * Text representation of sort component, as provided by query. - * @param dmoIface - * DMO interface. - * @param dmoClass - * DMO implementation class. * * @throws PersistenceException * if there is any error with the text representation of the sort * component provided by the caller. */ - SortCriterion(Dialect dialect, - Database database, - String text, - Class dmoIface, - Class dmoClass) + SortCriterion(Dialect dialect, String text, DmoMeta dmoInfo) throws PersistenceException { - this(dialect, database, text, dmoIface, dmoClass, true); + this(dialect, text, dmoInfo, true); } /** @@ -337,14 +320,10 @@ * * @param dialect * Database dialect. - * @param database - * The target database. * @param text * Text representation of sort component, as provided by query. - * @param dmoIface - * DMO interface. - * @param dmoClass - * DMO implementation class. + * @param dmoInfo + * Metadata for the table. * @param internStrings * {@code true} to intern string fields, else {@code false}. * @@ -352,24 +331,20 @@ * if there is any error with the text representation of the sort * component provided by the caller. */ - SortCriterion(Dialect dialect, - Database database, - String text, - Class dmoIface, - Class dmoClass, - boolean internStrings) + SortCriterion(Dialect dialect, String text, DmoMeta dmoInfo, boolean internStrings) throws PersistenceException { this.dialect = dialect; - this.database = database; - this.dmoClass = dmoClass; - String[] tmp = text.split(" "); - String rawName = tmp[0].trim(); + this.dmoClass = dmoInfo.getImplementationClass(); + + // parse the test and extract alias, property and optional extent + int spacePos = text.indexOf(' '); + String rawName = ((spacePos != -1) ? text.substring(0, spacePos) : text).trim(); name = rawName; - originalName = (internStrings ? rawName.intern() : rawName); - int startPos = rawName.indexOf("["); + originalName = internStrings ? rawName.intern() : rawName; + int bracketPos = rawName.indexOf("["); int endPos = -1; - if (startPos >= 0) + if (bracketPos >= 0) { endPos = rawName.indexOf("]"); if (endPos < 0) @@ -377,39 +352,30 @@ throw new PersistenceException("Malformed subscript text: " + text); } - // Extract base name. - name = rawName.substring(0, startPos); + // extract base name + name = rawName.substring(0, bracketPos); } - int dot = name.lastIndexOf("."); - if (dot < 0) + int dotPos = name.lastIndexOf("."); + if (dotPos < 0) { throw new PersistenceException("Sort property may not be unqualified: " + rawName); } - String propName = name.substring(dot + 1); - String alias = name.substring(0, dot); - - if (internStrings) - { - this.propertyName = propName.intern(); - this.alias = alias.intern(); - } - else - { - this.propertyName = propName; - this.alias = alias; - } - - if (startPos >= 0) - { - DmoMeta dmoMeta = DmoMetadataManager.getDmoInfo(dmoClass); - Property extProperty = dmoMeta.getFieldInfo(propertyName); - int extent = extProperty.index > 0 ? 0 : extProperty.extent; + String propName = name.substring(dotPos + 1); + String alias = name.substring(0, dotPos); + DmoMeta dmoMeta = DmoMetadataManager.getDmoInfo(dmoClass); + Property propMeta = dmoMeta.getFieldInfo(propName); + this.propertyName = propMeta.name; // already interned, anyway + this.alias = internStrings ? alias.intern() : alias; + + if (bracketPos >= 0) + { + int extent = propMeta.index > 0 ? 0 : propMeta.extent; if (extent > 0) { // extract subscript - String sub = rawName.substring(startPos + 1, endPos); + String sub = rawName.substring(bracketPos + 1, endPos); try { subscript = Integer.parseInt(sub); @@ -428,11 +394,11 @@ name = name.intern(); } - // Determine sort direction. - ascending = (tmp.length == 1 || !"desc".equalsIgnoreCase(tmp[1].trim())); + // determine sort direction + ascending = (spacePos == -1 || !"desc".equalsIgnoreCase(text.substring(spacePos + 1).trim())); - // Get backing getter method. - if (TemporaryBuffer.MULTIPLEX_FIELD_NAME.equals(propertyName)) + // get backing getter method + if (propMeta.id == ReservedProperty.ID_MULTIPLEX) { method = null; isCharacter = false; @@ -442,21 +408,21 @@ else { String ccPrefix = null; - Class iface = DatabaseManager.PRIMARY_KEY.equals(propertyName) - ? Persistable.class - : dmoIface; - method = PropertyHelper.allGettersByProperty(iface).get(propertyName); + method = propMeta.id == ReservedProperty.ID_PRIMARY_KEY + ? PropertyHelper.allGettersByProperty(Persistable.class).get(propertyName) + : propMeta.annMethod; - // Determine case sensitivity and computed column prefix, if needed. + // determine case sensitivity and computed column prefix, if needed boolean caseSens = false; - isCharacter = character.class.equals(method.getReturnType()); + isCharacter = propMeta._isCharacter; if (isCharacter) { Boolean ccIgnoreCase = null; if (dialect.needsComputedColumns()) { - ccIgnoreCase = DatabaseManager.getIgnoreCase(dmoClass, propertyName); + ccIgnoreCase = dmoMeta.isIndexedIgnoreCase(propertyName); } + if (ccIgnoreCase != null) { caseSens = !ccIgnoreCase; @@ -464,8 +430,9 @@ } else { - caseSens = DmoMetadataManager.isCaseSensitive(dmoIface, propertyName); + caseSens = propMeta.caseSensitive; } + if (ccPrefix != null && internStrings) { ccPrefix = ccPrefix.intern(); @@ -477,44 +444,34 @@ } /** - * Parse a sort phrase (not including the "order by" preamble) - * into a list of SortCriterion objects. All components of - * the phrase are assumed to be properties of the same DMO. + * Parse a sort phrase (not including the {@code "order by"} preamble) into a list of {@code SortCriterion} + * objects. All components of the phrase are assumed to be properties of the same DMO. * * @param dialect * Database dialect. - * @param database - * The target database. * @param sort * Sort phrase. -// * @param schema -// * Database schema. * @param dmoAlias * Alias qualifier which represents DMO in sort phrase. - * @param dmoClass - * DMO implementation class. - * @param dmoIface - * DMO interface. + * @param dmoInfo + * Metadata for the table. * @param makeUnique - * Indicates whether the <dmoAlias>.recid - * criterion should be appended to the returned criteria list. + * Indicates whether the {@code .recid} criterion should be appended to the returned + * criteria list. * - * @return List of SortCriterion objects. + * @return List of {@code SortCriterion} objects. * * @throws PersistenceException * if there is any error parsing the given string. */ public static List parse(Dialect dialect, - Database database, String sort, - /*String schema,*/ String dmoAlias, - Class dmoIface, - Class dmoClass, + DmoMeta dmoInfo, boolean makeUnique) throws PersistenceException { - CacheKey key = new CacheKey(dialect, sort, /*schema,*/ dmoAlias, dmoClass, makeUnique); + CacheKey key = new CacheKey(dialect, sort, dmoAlias, dmoInfo.getImplementationClass(), makeUnique); List list = null; synchronized (cacheLock) @@ -538,7 +495,7 @@ String[] sortComponents = sort.split(","); for (String sortComp : sortComponents) { - next = new SortCriterion(dialect, database, sortComp.trim(), /*schema,*/ dmoIface, dmoClass); + next = new SortCriterion(dialect, sortComp.trim(), dmoInfo); sortCriteria.add(next); sortNames.add(next.getUnqualifiedName()); } @@ -554,16 +511,15 @@ // criteria, if not redundant with the previous criteria. if (makeUnique && !sortNames.contains(DatabaseManager.PRIMARY_KEY)) { - String sortComp = dmoAlias + "." + DatabaseManager.PRIMARY_KEY + " " + - (pKeyAscend ? "asc" : "desc"); - sortCriteria.add(new SortCriterion(dialect, database, sortComp, /*schema,*/ dmoIface, dmoClass)); + String sortComp = dmoAlias + "." + DatabaseManager.PRIMARY_KEY + " " + (pKeyAscend ? "asc" : "desc"); + sortCriteria.add(new SortCriterion(dialect, sortComp, dmoInfo)); } // eliminate superfluous sort components if sort criteria define a superset of a unique // constraint. int size = sortCriteria.size(); int lastUniqueComp = size; - Iterator> uniques = DmoMetadataManager.uniqueConstraints(/*schema,*/ dmoIface); + Iterator> uniques = dmoInfo.getUniqueConstraints().iterator(); while (uniques.hasNext()) { Set compSet = uniques.next(); @@ -649,14 +605,7 @@ static List parse(String sort, RecordBuffer buffer, boolean makeUnique) throws PersistenceException { - Dialect dialect = buffer.getDialect(); - Database database = buffer.getDatabase(); -// String schema = buffer.getSchema(); - String dmoAlias = buffer.getDMOAlias(); - Class dmoClass = buffer.getDMOImplementationClass(); - Class dmoIface = buffer.getDMOInterface(); - - return parse(dialect, database, sort, /*schema,*/ dmoAlias, dmoIface, dmoClass, makeUnique); + return parse(buffer.getDialect(), sort, buffer.getDMOAlias(), buffer.dmoInfo, makeUnique); } /** === modified file 'src/com/goldencode/p2j/persist/SourceData.java' --- src/com/goldencode/p2j/persist/SourceData.java 2020-05-03 20:30:03 +0000 +++ src/com/goldencode/p2j/persist/SourceData.java 2021-01-29 00:53:41 +0000 @@ -2,14 +2,16 @@ ** Module : SourceData.java ** Abstract : Describes an external source from which data is read into a buffer. ** -** Copyright (c) 2017-2020, Golden Code Development Corporation. +** Copyright (c) 2017-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20170820 Created initial version. ** 002 ECF 20171008 Implemented file as an output resource. ** 003 ECF 20190120 Added implementation for c'tor which accepts longchar. ** 004 OM 20190327 Renamed to SourceData to avoid conflicts with DataSet source. ** 005 CA 20200503 Added c'tor to read data from a JsonObject instance (stubbed). +** 006 OM 20201120 Implementing Closeable interface. Lazily open the file stream. +** OM 20210120 Added full type support, including validation and error handling. */ /* @@ -25,39 +27,39 @@ ** ** You may find a copy of the GNU Affero GPL version 3 at the following ** location: https://www.gnu.org/licenses/agpl-3.0.en.html -** +** ** Additional terms under GNU Affero GPL version 3 section 7: -** +** ** Under Section 7 of the GNU Affero GPL version 3, the following additional ** terms apply to the works covered under the License. These additional terms ** are non-permissive additional terms allowed under Section 7 of the GNU ** Affero GPL version 3 and may not be removed by you. -** +** ** 0. Attribution Requirement. -** +** ** You must preserve all legal notices or author attributions in the covered ** work or Appropriate Legal Notices displayed by works containing the covered ** work. You may not remove from the covered work any author or developer ** credit already included within the covered work. -** +** ** 1. No License To Use Trademarks. -** +** ** This license does not grant any license or rights to use the trademarks ** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks ** of Golden Code Development Corporation. You are not authorized to use the ** name Golden Code, FWD, or the names of any author or contributor, for ** publicity purposes without written authorization. -** +** ** 2. No Misrepresentation of Affiliation. -** +** ** You may not represent yourself as Golden Code Development Corporation or FWD. -** +** ** You may not represent yourself for publicity purposes as associated with ** Golden Code Development Corporation, FWD, or any author or contributor to ** the covered work, without written authorization. -** +** ** 3. No Misrepresentation of Source or Origin. -** +** ** You may not represent the covered work as solely your work. All modified ** versions of the covered work must be marked in a reasonable way to make it ** clear that the modified work is not originating from Golden Code Development @@ -67,121 +69,339 @@ package com.goldencode.p2j.persist; +import com.goldencode.p2j.persist.serial.*; import com.goldencode.p2j.util.*; - import java.io.*; +import java.util.*; /** - * Object which describes an external source from which data is read into a temp-table buffer - * or data set member buffer. + * Object which describes an external source from which data is read into a temp-table buffer or dataset + * member buffer. */ public class SourceData +implements Serializator { /** Normalized input stream */ private InputStream stream = null; - /** - * Constructor. - * - * @param source - * Object from which to read data. This should be a - * {@link com.goldencode.p2j.oo.json.objectmodel.JsonConstruct}. - * WARNING: do not add generics to the parameter, as this creates problems in compile. - */ - public SourceData(object source) - { - UnimplementedFeature.missing("SourceData(jsonObject)"); - } - - /** - * Constructor. - * - * @param source - * Object from which to read data. - */ - public SourceData(character source) - { - initializeRemote(source.toStringMessage()); - } - - /** - * Constructor. - * - * @param source - * Object from which to read data. - */ - public SourceData(String source) - { - initializeRemote(source); - } - - /** - * Constructor. - * - * @param source - * Pointer to area of memory from which to read data. - */ - public SourceData(memptr source) - { - UnimplementedFeature.missing("SourceData c'tor"); - } - - /** - * Constructor. - * - * @param source - * Handle of object from which to read data. - */ - public SourceData(handle source) - { - UnimplementedFeature.missing("SourceData c'tor"); - } - - /** - * Constructor. - * - * @param source - * Object from which to read data. - */ - public SourceData(longchar source) - { - // TODO: making a copy of the backing byte array could pose a memory problem for large - // longchar values - stream = new ByteArrayInputStream(source.asByteArray(0, source.lengthOf())); - } - - /** - * Get the input stream representing the source resource from which data will be read. - * This may be a remote or a local resource. - * + /** The file name if such source was created. */ + private String fileName = null; + + /** + * Constant used to identify the calls from READ-XML method. Also used to store the supported stream types + * for this method. + */ + public static final String SD_READ_XML = "READ-XML"; + + /** + * Constant used to identify the calls from READ-XMLSCHEMA method. Also used to store the supported stream + * types for this method. + */ + public static final String SD_READ_XMLSCHEMA = "READ-XMLSCHEMA"; + + /** + * Constant used to identify the calls from READ-JSON method. Also used to store the supported stream types + * for this method. + */ + public static final String SD_READ_JSON = "READ-JSON"; + + /* The initialization of static member data. */ + private static final Map supportedStreams = new HashMap<>(); + static + { + supportedStreams.put(SD_READ_XML, SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR); + supportedStreams.put(SD_READ_XMLSCHEMA, SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR); + supportedStreams.put(SD_READ_JSON, + SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR | SER_JSON_ARRAY | SER_JSON_OBJECT); + } + + /** Local copy of the stream type configured in constructor. */ + private final String origType; + + /** Local reference to stream (or stream name), as configured in constructor. */ + private final BaseDataType origSrc; + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param source + * The reference to resource or the file name. + */ + public SourceData(character type, BaseDataType source) + { + this((type == null) ? null : type.toJavaType(), source); + } + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param source + * The reference to resource or the file name. + */ + public SourceData(String type, BaseDataType source) + { + origType = type; + origSrc = source; + } + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param source + * The file name. + */ + public SourceData(String type, String source) + { + origType = type; + origSrc = new character(source); + } + + /** + * Configures the resource stream types for the current usage. The caller passes in its name and the + * method does the selection. It also does the verification whether the already configured parameters (in + * the constructor) are valid and eventual raise the error condition. + * + * @param method + * The identifier for the method using the source stream. It is used to get the set of acceptable + * stream types to check against them. + * @param widgetType + * The parent widget. Only used for composing the error messages. + * + * @return {@code true} if the parameters configured in constructor seems correct. This method does not + * check the content, only the variable types. If there is not a match with the possible streams + * supported by the {@code method}, {@code false} is returned, after eventually raising a specific + * error condition. + */ + public boolean configureSupportedSources(String method, String widgetType) + { + boolean isJson = method.contains("JSON"); + if (origType == null) + { + ErrorManager.recordOrShowError(5442, method, "character"); + // Invalid datatype for argument to method ''. Expecting '' + return false; + } + + if (origSrc == null || origSrc.isUnknown()) + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + + // [method] must always be one of the supported methods + int supportedSources = supportedStreams.get(method); + + switch (origType.toUpperCase()) + { + case TYPE_FILE: + if ((supportedSources & SER_FILE) != 0) + { + if ((origSrc instanceof longchar)) + { + // no error, but false + return false; + } + + if (!(origSrc instanceof character)) + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + return true; + } + break; + + case TYPE_STREAM: + if ((supportedSources & SER_STREAM) != 0) + { + return true; + } + break; + + case TYPE_STREAM_HANDLE: + if ((supportedSources & SER_STREAM_HANDLE) != 0) + { + return true; + } + break; + + case TYPE_MEMPTR: + if ((supportedSources & SER_MEMPTR) != 0) + { + if (!(origSrc instanceof memptr)) + { + if (isJson) + { + ErrorManager.recordOrShowError(15362); + // READ-JSON source is not a valid LONGCHAR or MEMPTR. (15362) + ErrorManager.recordOrShowError(17956); + // Unable to create reader for READ-JSON (17956) + } + else + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + } + return false; + } + return true; + } + break; + + case TYPE_HANDLE: + if (isJson) + { + ErrorManager.recordOrShowError(15356); + // Handle type not valid as JSON input source. (15356) + return false; + } + else + { + ErrorManager.recordOrShowError(10515); + // Handle type not valid as XML input source. (10515) + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + + case TYPE_LONGCHAR: + if ((supportedSources & SER_LONGCHAR) != 0) + { + if (!(origSrc instanceof longchar)) + { + if (isJson) + { + ErrorManager.recordOrShowError(15362); + // READ-JSON source is not a valid LONGCHAR or MEMPTR. (15362) + ErrorManager.recordOrShowError(17956); + // Unable to create reader for READ-JSON (17956) + } + else + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + return false; + } + return true; + } + break; + + case TYPE_JSON_OBJECT: + if ((supportedSources & SER_JSON_OBJECT) != 0) + { + return true; + } + break; + + case TYPE_JSON_ARRAY: + if ((supportedSources & SER_JSON_ARRAY) != 0) + { + return true; + } + break; + } + + ErrorManager.recordOrShowError(isJson ? 15357 : 13184, origType); + // err 13184: Invalid source-type for READ-XML: . + // err 15357: Invalid source-type for READ-JSON: . + return false; + } + + /** + * Get the input stream representing the source resource from which data will be read. This may be backed + * by a remote or a local resource. + * * @return Input stream. + * + * @throws IOException + * if the method encounters issues with the resource. */ - public InputStream getInputStream() + @Override + public InputStream getStream() + throws IOException { - return stream; + // if already built, return it again + if (stream != null) + { + return stream; + } + + switch (origType.toUpperCase()) + { + case TYPE_FILE: + if (origSrc instanceof character) + { + fileName = ((character) origSrc).toJavaType(); + try + { + stream = new InputStreamWrapper(StreamFactory.openFileStream(fileName, false, false)); + } + catch (ErrorConditionException ece) + { + throw new FileNotFoundException(fileName); + } + return stream; + } + return null; + + case TYPE_LONGCHAR: + if (origSrc instanceof longchar) + { + longchar source = (longchar) origSrc; + stream = new ByteArrayInputStream(source.asByteArray(0, source.lengthOf())); + return stream; + } + return null; + + case TYPE_MEMPTR: + if (origSrc instanceof memptr) + { + memptr source = (memptr) origSrc; + stream = new ByteArrayInputStream(source.asByteArray(0, source.lengthOf())); + return stream; + } + return null; + } + + return null; } /** * Close the input stream underlying this data source. - * + * * @throws IOException * if there is an error closing the stream. */ - void close() + public void close() throws IOException { - stream.close(); + if (stream != null) + { + stream.close(); + } } /** - * Initialize the remote input stream which backs this data source. - * - * @param filename - * Client-side file name. + * Obtain the file name of the source, if one was used to create the source. + * + * @return the file name of the source, if one was used to create the source. Otherwise {@code null}. */ - private void initializeRemote(String filename) + public String getFileName() { - stream = new InputStreamWrapper(StreamFactory.openFileStream(filename, false, false)); + return fileName; } } === modified file 'src/com/goldencode/p2j/persist/StaticTempTable.java' --- src/com/goldencode/p2j/persist/StaticTempTable.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/StaticTempTable.java 2021-01-28 20:00:25 +0000 @@ -2,7 +2,7 @@ ** Module : StaticTempTable.java ** Abstract : Static 4GL temp-table object. ** -** Copyright (c) 2013-2020, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 SVL 20131115 Created initial version. Most of TODOs is just about throwing a proper error. @@ -26,8 +26,12 @@ ** 016 SVL 20190801 Added get/setCodePageSupplier. ** 017 ECF 20200906 New ORM implementation. ** 018 AIL 20200914 Implemented _prepared abstract method. -** 019 AIL 20200915 Moved prepared method to superclass. +** AIL 20200915 Moved prepared method to superclass. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201120 Extracted SERIALIZE-NAME to dedicated interface. Implemented CLEAR state awareness. +** OM 20210106 Fixed 2nd parameter of TEMP-TABLE-PREPARE. +** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local +** dmoMeta.legacyTable. */ /* @@ -97,7 +101,6 @@ */ public class StaticTempTable extends AbstractTempTable -implements NamedSerializable { /** * Code pages of CLOB fields keyed by hibernate property names. @@ -108,7 +111,7 @@ * Code page suppliers of CLOB fields keyed by hibernate property names. */ private Map codePagesSuppliers; - + /** * Default constructor. * @@ -121,11 +124,11 @@ defaultBufferHandle = new handle(); defaultBufferHandle.assign(defaultBuffer); + TemporaryBuffer buffer = (TemporaryBuffer) ((BufferImpl) defaultBuffer).buffer(); TableMapper.mapTemporaryTable(this); - String name = TableMapper.getLegacyName(this); - this.name.assign(name); - super.undoable = ((BufferImpl)defaultBuffer).buffer().isUndoable(); + this.name.assign(buffer.dmoInfo.legacyTable); + super.undoable = buffer.isUndoable(); } /** @@ -763,8 +766,7 @@ @Override public logical tempTablePrepare(character name) { - displayPreparedIgnoring(); - return new logical(false); + return tempTablePrepare((String) null); } /** @@ -773,8 +775,7 @@ * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. * * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. + * Temp-table name to be used in subsequent query statements which refer to this temp-table. * @param before * Create the {@code BEFORE-TABLE} also if {@code true}. * @@ -783,18 +784,52 @@ @Override public logical tempTablePrepare(String name, boolean before) { - displayPreparedIgnoring(); - return new logical(false); - } - - /** - * Signals that all the field and index definitions for a temp-table have been provided. After - * the call to this method no fields and indexes can be added to this temporary table. - * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. - * - * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. + return tempTablePrepare((String) null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(String name, logical before) + { + return tempTablePrepare((String) null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(character name, boolean before) + { + return tempTablePrepare((String) null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. * @param before * Create the {@code BEFORE-TABLE} also if {@code true}. * @@ -803,8 +838,7 @@ @Override public logical tempTablePrepare(character name, logical before) { - displayPreparedIgnoring(); - return new logical(false); + return tempTablePrepare((String) null); } /** @@ -814,12 +848,28 @@ * * @return true if operation is successful */ - protected boolean _prepared() + public boolean _prepared() { return true; } /** + * Check whether this TEMP-TABLE is in CLEAR state: I.e. the temp-table is first created or immediately + * after the CLEAR() method is applied. The other two state of a temp-table are: UNPREPARED and PREPARED. + * The UNPREPARED state between the first definitional method has been applied and before the + * TEMP-TABLE-PREPARE() method is applied. + *

+ * This method is FWD-internal. The programmer can check whether the temp-table is in an UNPREPARED or + * PREPARED state by checking the PREPARED attribute, but the CLEAR state is transparent. + * + * @return Always {@code false} since the static temp-tables cannot be CLEAR-ed. + */ + public boolean _clear() + { + return false; + } + + /** * Obtain the current UNDO attribute of 4GL temp-table. * * @return true if the the temp-table can undo. @@ -1258,44 +1308,6 @@ { return false; } - - /** - * Implementation of the read access of the {@code SERIALIZE-NAME} attribute. - * - * @return the current value of the {@code SERIALIZE-NAME} attribute. - */ - @Override - public character getSerializeName() - { - String sName = TableMapper.getSerializeOptions(this).getSerializeName(); - String serializeName = sName == null || sName.trim().isEmpty() ? name.getValue() : sName; - - return new character(serializeName); - } - - /** - * Implementation of the write access of the {@code SERIALIZE-NAME} attribute. - * - * @param sName - * the new value for the {@code SERIALIZE-NAME} attribute. - */ - @Override - public void setSerializeName(Text sName) - { - setSerializeName(sName == null || sName.isUnknown() ? null : sName.getValue()); - } - - /** - * Implementation of the write access of the {@code SERIALIZE-NAME} attribute. - * - * @param sName - * the new value for the {@code SERIALIZE-NAME} attribute. - */ - @Override - public void setSerializeName(String sName) - { - TableMapper.getSerializeOptions(this).setSerializeName(sName == null ? "" : sName); - } /** * Set code page supplier for the specified CLOB field. === modified file 'src/com/goldencode/p2j/persist/TableMapper.java' --- src/com/goldencode/p2j/persist/TableMapper.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/TableMapper.java 2021-01-29 00:53:41 +0000 @@ -2,7 +2,7 @@ ** Module : TableMapper.java ** Abstract : Table and field name to converted DMO and property name (bidirectional). ** -** Copyright (c) 2013-2020, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 CA 20131015 Created initial version. @@ -74,6 +74,12 @@ ** OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** SVL 20201111 Support for null LABEL and COLUMN-LABEL. +** OM 20201029 Added case sensitive column support. +** OM 20201110 TableSerializeOptions are volative and cannot be cached. +** OM 20210122 Temp-table attributes in LegacyFieldInfo can be modified. +** OM 20210127 Replaced TableMapper.getLegacyName() (now deprecated) triple map lookup with direct access +** to local dmoMeta.legacyTable. */ /* @@ -163,8 +169,7 @@ * @param * The type of the key used by the {@link #p2j progress-to-java} map. For permanent * DMOs, this will be a {@link String}. For temporary DMOs, this will be an - * {@link Integer} value with the UNIQUE-ID identifying the - * {@link TempTable} resource. + * {@link Integer} value with the {@code UNIQUE-ID} identifying the {@link TempTable} resource. */ public abstract class TableMapper { @@ -256,29 +261,14 @@ * A buffer instance. * * @return The legacy table name. + * + * @deprecated Use the {@code DmoMeta.legacyTable} directly instead. */ + @Deprecated public static String getLegacyName(Buffer buffer) { RecordBuffer buf = ((BufferImpl) buffer).buffer(); - if (buf.isTemporary()) - { - TempTable tt = buf.getParentTable(); - String name = tt.name().getValue(); - if (name != null) - { - // return the name of the parent table - return name; - } - else - { - // name not yet assigned to a static temp table, get it from the mapping - return getLegacyName(tt); - } - } - else - { - return getLegacyName(buf.getDMOInterface()); - } + return buf.dmoInfo.legacyTable; } /** @@ -865,40 +855,8 @@ */ public static String getLegacySchemaName(Class dmoIface) { - String schema = DmoMetadataManager.getDmoInfo(dmoIface).getSchema(); - - return schema + "." + getLegacyName(dmoIface); - } - - /** - * Get the legacy table name for the given DMO, which is part of a permanent DB. - * - * @param dmoIface - * The DMO interface for a permanent DMO. - * - * @return The legacy table name. - * - * @throws IllegalStateException - * If a DMO belonging to the {@link DatabaseManager#TEMP_TABLE_SCHEMA} temp schema - * is passed as a parameter; for these cases, {@link #getLegacyName(TempTable)} - * or {@link #getLegacyName(Buffer)} must be used. - */ - public static String getLegacyName(Class dmoIface) - { DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface); - if (dmoInfo.isTempTable()) - { - String msg = "Legacy table name for a temporary DMO can not be resolved using this API!"; - - throw new IllegalArgumentException(msg); - } - - synchronized (permanentDBs) - { - PermanentTableMapper mapper = locatePerm(dmoInfo.getSchema()); - - return mapper.legacyName(dmoIface); - } + return dmoInfo.getSchema() + "." + dmoInfo.legacyTable; } /** @@ -931,75 +889,6 @@ } /** - * Get the legacy table serialize options for the DMO referenced by the given buffer. - * - * @param buffer - * A buffer instance. - * - * @return The serialize options. - */ - public static TableSerializeOptions getSerializeOptions(Buffer buffer) - { - RecordBuffer buf = ((BufferImpl) buffer).buffer(); - if (buf.isTemporary()) - { - TempTable tt = buf.getParentTable(); - return getSerializeOptions(tt); - } - else - { - return getSerializeOptions(buf.getDMOInterface()); - } - } - - /** - * Get the legacy serialize options for the given DMO, which is part of a permanent DB. - *

- * This API always throws an {@link IllegalStateException}, as serialize options can be defined - * only for temp-tables. - * - * @param dmoIface - * The DMO interface for a permanent DMO. - * - * @return n/a. - * - * @throws IllegalStateException - * Always. - */ - public static TableSerializeOptions getSerializeOptions(Class dmoIface) - { - String msg; - if (DmoMetadataManager.getDmoInfo(dmoIface).isTempTable()) - { - msg = "Legacy serialize options for a temporary DMO can not be resolved using this API!"; - } - else - { - msg = "Legacy serialize options are not available for a permanent DMO!"; - } - - throw new IllegalArgumentException(msg); - } - - /** - * Get the legacy serialize options for the given {@link TempTable} resource. - * - * @param tt - * The {@link TempTable} resource. - * - * @return The legacy serialize options. - */ - public static TableSerializeOptions getSerializeOptions(TempTable tt) - { - TempTableMapper mapper = locateTemp(tt); - - synchronized (mapper) - { - return mapper.serializeOptions(tt); - } - } - - /** * Get the legacy field name for the given property, which is defined by a DMO part of a * permanent DB. * @@ -1083,7 +972,6 @@ } } - /** * Get the 4GL name of the extent field represented by the given property. * @@ -1212,24 +1100,6 @@ } /** - * Get the legacy table name for the given {@link TempTable} resource. - * - * @param tt - * The {@link TempTable} resource. - * - * @return The legacy table name. - */ - public static String getLegacyName(TempTable tt) - { - TempTableMapper mapper = locateTemp(tt); - - synchronized (mapper) - { - return mapper.legacyName(tt); - } - } - - /** * Get the legacy field name for the given {@link TempTable} resource and property. * * @param tt @@ -1717,7 +1587,7 @@ String schema = dmoInfo.getSchema(); // - mapping is kept by schema - // - permamentDBs are kept in a global map + // - permanentDBs are kept in a global map synchronized (permanentDBs) { PermanentTableMapper mapper = locatePerm(schema); @@ -2705,6 +2575,7 @@ { Integer key = getLegacyKey(tt); p2j.remove(key); + j2p.remove(tt); } /** @@ -2724,22 +2595,6 @@ } /** - * Get the legacy serialize options for the given {@link TempTable} resource. - * - * @param tt - * The {@link TempTable} resource. - * - * @return The legacy serialize options. - */ - protected TableSerializeOptions serializeOptions(TempTable tt) - { - Integer key = getLegacyKey(tt); - LegacyTableInfo info = p2j.get(key); - - return info.serializeOptions; - } - - /** * Get the mapping of temp-table IDs to {@link LegacyTableInfo} instances. * * @return Map of temp-table IDs to table information objects. @@ -2940,9 +2795,6 @@ /** The DMO implementation class. */ private final Class dmoClass; - /** The temp-table serialize options. */ - private final TableSerializeOptions serializeOptions; - /** true if the table was defined with LIKE-SEQUENTIAL option. */ private final boolean likeSequential; @@ -2956,7 +2808,7 @@ * @param dmoInfo * Object which contains metadata about the DMO. * - * @see #loadFields(Class) + * @see #loadFields(DmoMeta) */ private LegacyTableInfo(DmoMeta dmoInfo) { @@ -2965,11 +2817,7 @@ this.dmoClass = dmoInfo.getImplementationClass(); this.validation = dmoInfo.hasValidation; this.likeSequential = dmoInfo.isLikeSequential; - this.serializeOptions = new TableSerializeOptions(dmoInfo.serializeName, - dmoInfo.xmlNodeName, - dmoInfo.namespaceUri, - dmoInfo.namespacePrefix); - loadFields(dmoIface); + loadFields(dmoInfo); List fieldList = new ArrayList<>(j2p.values()); Collections.sort(fieldList); @@ -3032,15 +2880,14 @@ /** * Load all fields from the specified class and add their mappings. * - * @param dmoIface - * The DMO interface or a composite class with the extent fields. + * @param dmoInfo + * The DMO data for the interface or a composite class with the extent fields. * * @throws NullPointerException * If a DMO property does not have a {@link Property} annotation. */ - private void loadFields(Class dmoIface) + private void loadFields(DmoMeta dmoInfo) { - DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface); for (Iterator it = dmoInfo.getFields(true); it.hasNext(); ) { Property fieldAnn = it.next(); @@ -3159,31 +3006,34 @@ boolean firstSeen = fieldInfo == null; if (firstSeen || fieldInfo.fieldId != fieldId) { - fieldInfo = new LegacyFieldInfo(columnLabel, - fieldId, - format, - help, - initial, - decimals, - javaName, - label, - legacyName, - mandatory, - extent, - order, - position, - original, - validateMessage, - validateExpression, - (Class) dataType, - initialValue, - serializeHidden, - serializeName, - xmlDataType, - xmlNodeName, - xmlNodeType, - like, - codePage); + fieldInfo = new LegacyFieldInfo( + dmoInfo.tempTable, + columnLabel, + fieldId, + format, + help, + initial, + decimals, + javaName, + label, + legacyName, + mandatory, + extent, + order, + position, + original, + validateMessage, + validateExpression, + (Class) dataType, + initialValue, + serializeHidden, + serializeName, + xmlDataType, + xmlNodeName, + xmlNodeType, + like, + codePage, + fieldAnn.caseSensitive); } if (firstSeen) // for extent field store the first representative info only @@ -3208,31 +3058,34 @@ javaNames.put(extentIndex, fieldInfo); if (extents.get(original) == null) { - LegacyFieldInfo extentInfo = new LegacyFieldInfo(columnLabel, - fieldId, - format, - help, - initial, - decimals, - original, - label, - legacyName, - mandatory, - extent, - order, - position, - "", - validateMessage, - validateExpression, - (Class) dataType, - initialValue, - serializeHidden, - serializeName, - xmlDataType, - xmlNodeName, - xmlNodeType, - like, - codePage); + LegacyFieldInfo extentInfo = new LegacyFieldInfo( + dmoInfo.tempTable, + columnLabel, + fieldId, + format, + help, + initial, + decimals, + original, + label, + legacyName, + mandatory, + extent, + order, + position, + "", + validateMessage, + validateExpression, + (Class) dataType, + initialValue, + serializeHidden, + serializeName, + xmlDataType, + xmlNodeName, + xmlNodeType, + like, + codePage, + fieldAnn.caseSensitive); extents.put(original, extentInfo); } } @@ -3386,7 +3239,7 @@ private final String fieldLegacyName; /** Determines if this component has descending sorting. */ - private boolean effectiveDescending; + private final boolean effectiveDescending; /** * Create a new index component information container, holding field name and sorting @@ -3461,8 +3314,8 @@ private final String initial; /** DECIMALS attribute of this field. */ - private final int decimals; - + private int decimals; + /** The associated DMO property name. */ private final String javaName; @@ -3473,13 +3326,13 @@ private final String legacyName; /** EXTENT attribute of the field (from the legacy schema). */ - private int extent; + private final int extent; /** ORDER attribute of the field (from the legacy schema). */ - private int order; + private final int order; /** POSITION attribute of the field (from the legacy schema). */ - private int position; + private final int position; /** LITERAL-QUESTION attribute of this field. */ private boolean literalQuestion = false; @@ -3504,20 +3357,30 @@ /** Options needed when serializing temp-table data to/from external media */ private final SerializeOptions serializeOptions; - + /** * Fully qualified name of the source field from which the field definitions were copied * using LIKE option. */ private final String like; - + /** Code page of the CLOB field. */ private final String codePage; + /** Flags case-sensitive character fields. */ + private final boolean caseSensitive; + + /** Flags mutable objects. */ + private final boolean mutable; + /** - * Create a new field information container, holding the legacy and converted names and - * field attributes. + * Create a new field information container, holding the legacy and converted names and field + * attributes. * + * @param mutable + * If this structure is mutable. This is the case of temp-tables, which allow some of theirs + * field properties to be changed programmatically, at runtime. Only the mutable attributes + * have setters and their implementation must check this flag. * @param columnLabel * COLUMN-LABEL attribute. * @param fieldId @@ -3569,40 +3432,45 @@ * copied using LIKE option. * @param codePage * Code page of the CLOB field. + * @param caseSensitive + * Flags case-sensitive character fields. */ - public LegacyFieldInfo(String columnLabel, - Integer fieldId, - String format, - String help, - String initial, - int decimals, - String javaName, - String label, - String legacyName, - boolean mandatory, - int extent, - int order, - int position, - String original, - String validateMessage, - String validateExpression, - Class dataType, - BaseDataType initialValue, - boolean serializeHidden, - String serializeName, - String xmlDataType, - String xmlNodeName, - String xmlNodeType, - String like, - String codePage) + private LegacyFieldInfo(boolean mutable, + String columnLabel, + Integer fieldId, + String format, + String help, + String initial, + int decimals, + String javaName, + String label, + String legacyName, + boolean mandatory, + int extent, + int order, + int position, + String original, + String validateMessage, + String validateExpression, + Class dataType, + BaseDataType initialValue, + boolean serializeHidden, + String serializeName, + String xmlDataType, + String xmlNodeName, + String xmlNodeType, + String like, + String codePage, + boolean caseSensitive) { - this.columnLabel = columnLabel.intern(); + this.mutable = mutable; + this.columnLabel = columnLabel == null ? null : columnLabel.intern(); this.fieldId = fieldId; this.help = help.intern(); this.initial = initial == null ? null : initial.intern(); this.decimals = decimals; this.javaName = javaName.intern(); - this.label = label.intern(); + this.label = label == null ? null : label.intern(); this.legacyName = legacyName.intern(); this.mandatory = mandatory; this.extent = extent; @@ -3611,6 +3479,7 @@ this.original = original.intern(); this.validateMessage = validateMessage; this.validateExpression = validateExpression; + this.caseSensitive = caseSensitive; String defaultFormat; @@ -3862,6 +3731,44 @@ } /** + * Checks whether this is a case-sensitive character field. + * + * @return {@code true} only if this is a case-sensitive character field. + */ + public boolean isCaseSensitive() + { + return caseSensitive; + } + + /** + * Obtain the precision for decimal fields. + * + * @return the precision for decimal fields. + */ + public int getDecimals() + { + return decimals; + } + + /** + * Sets the precision for decimal fields. Only for mutable objects. + * + * @param decimals + * The new precision for decimal fields. + * + * @throws IllegalStateException + * if the object is not declared as mutable. + */ + public void setDecimals(int decimals) + { + if (!mutable) + { + throw new IllegalStateException("Object not mutable"); + } + this.decimals = decimals; + } + + /** * Compare this field info object to another, based on its natural order. Natural order is * determined by field IDs. * @@ -3874,5 +3781,24 @@ { return this.fieldId - o.fieldId; } + + /** + * Obtain a short sreing representation of the object used in debugging. + * + * @return a short sreing representation of the object used in debugging. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("LFI{"); + sb.append(legacyName).append("/").append(javaName); + sb.append("(").append(fieldId).append(")").append(":").append(dataType.getSimpleName()); + if (extent != 0) + { + sb.append("[").append(extent).append("]"); + } + sb.append('}'); + return sb.toString(); + } } } === modified file 'src/com/goldencode/p2j/persist/TableWrapper.java' --- src/com/goldencode/p2j/persist/TableWrapper.java 2020-09-14 12:05:37 +0000 +++ src/com/goldencode/p2j/persist/TableWrapper.java 2021-01-13 21:04:41 +0000 @@ -1,11 +1,10 @@ /* ** Module : TableWrapper.java -** Abstract : A wrapper for table result sets, used to serialize a table data and send it to a -* remote side. +** Abstract : A wrapper for table result sets, used to serialize a table data and send it to a remote side. ** ** Copyright (c) 2013-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 CA 20130620 Created initial version. ** 002 SVL 20140611 Fixed NPE in writeExternal when no result set is transferred. Added tableName. ** 003 SVL 20140797 Added tableHandle field. @@ -19,6 +18,7 @@ ** CA 20200514 In some cases, BLOB fields are already MemoryBuffer. ** 008 ECF 20200906 New ORM implementation. ** 009 IAS 20200908 Rework (de)serialization. +** OM 20201120 Added implementation of XML-NODE-NAME and ERROR-STRING attributes */ /* @@ -136,6 +136,12 @@ /** The XML prefix for this table definition. */ private String xmlPrefix = null; + /** The XML-NODE-NAME attribute for this table definition. */ + private String xmlNodeName = null; + + /** The ERROR-STRING attribute for this table definition. */ + private String errorString = null; + /** * A string with all indexes of this table definition encoded as a {@code String}. * @@ -377,6 +383,48 @@ } /** + * Obtain the XML node name for this table definition. + * + * @return the XML node name for this table definition. + */ + public String getXmlNodeName() + { + return xmlNodeName; + } + + /** + * Sets the XML node name for this table definition. + * + * @param xmlNodeName + * XML prefix for this table definition. + */ + public void setXmlNodeName(String xmlNodeName) + { + this.xmlNodeName = xmlNodeName; + } + + /** + * Obtain the ERROR-STRING attribute for this table definition. + * + * @return the ERROR-STRING attribute for this table definition. + */ + public String getErrorString() + { + return errorString; + } + + /** + * Sets the ERROR-STRING attribute for this table definition. + * + * @param err + * ERROR-STRING attribute for this table definition. + */ + public void setErrorString(String err) + { + this.errorString = err; + } + + /** * Provides an iterator over the read {@link #properties}. This is used to access the data * sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resultSet} * is set. @@ -741,8 +789,8 @@ Buffer defBuff = (Buffer) table.defaultBufferHandle().getResource(); this.indexes = TableMapper.getLegacyIndexInfo(defBuff); this.numIndexes = indexes.split(".").length; - this.xmlns = defBuff.getNamespaceURI().toStringMessage(); - this.xmlPrefix = defBuff.getNamespacePrefix().toStringMessage(); + this.xmlns = defBuff.namespaceURI().toStringMessage(); + this.xmlPrefix = defBuff.namespacePrefix().toStringMessage(); } /** === modified file 'src/com/goldencode/p2j/persist/TargetData.java' --- src/com/goldencode/p2j/persist/TargetData.java 2020-09-10 09:54:53 +0000 +++ src/com/goldencode/p2j/persist/TargetData.java 2021-01-29 00:53:41 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2017-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20170820 Created initial version. ** 002 CA 20170830 Added missing c'tor. ** 003 ECF 20171008 Rolled back #002. Implemented file as an output resource. @@ -14,6 +14,8 @@ ** 007 CA 20190720 byte[] must be converted to String before copying it. ** CA 20190811 Added memptr support. ** 008 CA 20200910 Fixed Json c'tors - these can be any object, the type is checked at runtime. +** OM 20201120 Implemented Closable interface. The file stream is lazily opened. +** OM 20210120 Added full type support, including validation and error handling. */ /* @@ -29,39 +31,39 @@ ** ** You may find a copy of the GNU Affero GPL version 3 at the following ** location: https://www.gnu.org/licenses/agpl-3.0.en.html -** +** ** Additional terms under GNU Affero GPL version 3 section 7: -** +** ** Under Section 7 of the GNU Affero GPL version 3, the following additional ** terms apply to the works covered under the License. These additional terms ** are non-permissive additional terms allowed under Section 7 of the GNU ** Affero GPL version 3 and may not be removed by you. -** +** ** 0. Attribution Requirement. -** +** ** You must preserve all legal notices or author attributions in the covered ** work or Appropriate Legal Notices displayed by works containing the covered ** work. You may not remove from the covered work any author or developer ** credit already included within the covered work. -** +** ** 1. No License To Use Trademarks. -** +** ** This license does not grant any license or rights to use the trademarks ** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks ** of Golden Code Development Corporation. You are not authorized to use the ** name Golden Code, FWD, or the names of any author or contributor, for ** publicity purposes without written authorization. -** +** ** 2. No Misrepresentation of Affiliation. -** +** ** You may not represent yourself as Golden Code Development Corporation or FWD. -** +** ** You may not represent yourself for publicity purposes as associated with ** Golden Code Development Corporation, FWD, or any author or contributor to ** the covered work, without written authorization. -** +** ** 3. No Misrepresentation of Source or Origin. -** +** ** You may not represent the covered work as solely your work. All modified ** versions of the covered work must be marked in a reasonable way to make it ** clear that the modified work is not originating from Golden Code Development @@ -72,9 +74,8 @@ package com.goldencode.p2j.persist; import java.io.*; - -import com.goldencode.p2j.oo.json.objectmodel.JsonConstruct; -import com.goldencode.p2j.oo.lang._BaseObject_; +import java.util.*; +import com.goldencode.p2j.persist.serial.*; import com.goldencode.p2j.util.*; /** @@ -82,234 +83,315 @@ * or data set member buffer. */ public class TargetData +implements Serializator { /** Normalized output stream */ private OutputStream stream; - /** - * Constructor. - * - * @param type - * {@code FILE} or {@code STREAM}. - * @param target - * Name of a file or stream to which data is written. - */ - public TargetData(character type, character target) - { - initializeRemote(type.toStringMessage(), target.toStringMessage()); - } - - /** - * Constructor. - * - * @param type - * {@code FILE} or {@code STREAM}. - * @param target - * Name of a file or stream to which data is written. - */ - public TargetData(String type, character target) - { - initializeRemote(type, target.toStringMessage()); - } - - /** - * Constructor. - * - * @param type - * {@code FILE} or {@code STREAM}. - * @param target - * Name of a file or stream to which data is written. - */ - public TargetData(character type, String target) - { - initializeRemote(type.toStringMessage(), target); - } - - /** - * Constructor. - * - * @param type - * {@code FILE} or {@code STREAM}. - * @param target - * Name of a file or stream to which data is written. + /** The file name if such destination was created. */ + private String fileName = null; + + /** + * Constant used to identify the calls from WRITE-XML method. Also used to store the supported stream types + * for this method. + */ + public static final String TD_WRITE_XML = "WRITE-XML"; + + /** + * Constant used to identify the calls from WRITE-XMLSCHEMA method. Also used to store the supported stream + * types for this method. + */ + public static final String TD_WRITE_XMLSCHEMA = "WRITE-XMLSCHEMA"; + + /** + * Constant used to identify the calls from WRITE-JSON method. Also used to store the supported stream + * types for this method. + */ + public static final String TD_WRITE_JSON = "WRITE-JSON"; + + /** + * Constant used to identify the calls from SERIALIZE-ROW method. Also used to store the supported stream + * types for this method. + */ + public static final String TD_SERIALIZE_ROW = "SERIALIZE-ROW"; + + /* The initialization of static member data. */ + private static final Map supportedStreams = new HashMap<>(); + static + { + supportedStreams.put(TD_WRITE_XML, + SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR); + supportedStreams.put(TD_WRITE_XMLSCHEMA, + SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR); + supportedStreams.put(TD_WRITE_JSON, + SER_FILE | SER_MEMPTR | SER_HANDLE | SER_LONGCHAR | SER_JSON_ARRAY | SER_JSON_OBJECT); + supportedStreams.put(TD_SERIALIZE_ROW, + SER_FILE | SER_STREAM | SER_STREAM_HANDLE | SER_MEMPTR | SER_LONGCHAR | SER_JSON_OBJECT); + } + + private final String origType; + private final BaseDataType origDst; + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param target + * The reference to resource or the file name. + */ + public TargetData(character type, BaseDataType target) + { + this((type == null) ? null : type.toJavaType(), target); + } + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param target + * The reference to resource or the file name. + */ + public TargetData(String type, BaseDataType target) + { + origType = type; + origDst = target; + } + + /** + * The constructor saves the configured parameters but does not process them in any way. The validation + * will be executed later, when the calling method is in progress. + * + * @param type + * The source stream type. + * @param target + * The file name. */ public TargetData(String type, String target) { - initializeRemote(type, target); - } - - /** - * Constructor. - * - * @param target - * Pointer to an area of memory into which data is written. - */ - public TargetData(memptr target) - { - // TODO: better way to do this than overriding close()? Overriding flush() may cause to - // many assigns to the target longchar - stream = new ByteArrayOutputStream() - { - @Override - public void close() - throws IOException - { - flush(); - super.close(); - byte[] buf = toByteArray(); - target.assign(buf); - } - }; - } - - /** - * Constructor. - * - * @param type - * {@code STREAM-HANDLE} or {@code HANDLE}. - * @param target - * Handle of a stream object or X-document/X-noderef object into which to write data. - */ - public TargetData(character type, handle target) - { - initializeRemote(type.toStringMessage(), target); - } - - /** - * Constructor. - * - * @param type - * {@code STREAM-HANDLE} or {@code HANDLE}. - * @param target - * Handle of a stream object or X-document/X-noderef object into which to write data. - */ - public TargetData(String type, handle target) - { - initializeRemote(type, target); - } - - /** - * Constructor. - * - * @param type - * {@code STREAM-HANDLE} or {@code HANDLE}. - * @param target - * The target JsonObject or JsonArray instance. - */ - public TargetData(character type, object target) - { - UnimplementedFeature.todo("BUFFER:{SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray) is not implemented."); - } - - /** - * Constructor. - * - * @param type - * {@code STREAM-HANDLE} or {@code HANDLE}. - * @param target - * The target JsonObject or JsonArray instance. - */ - public TargetData(String type, object target) - { - UnimplementedFeature.todo("BUFFER:{SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray) is not implemented."); - } - - /** - * Constructor. - * - * @param target - * Variable into which to write data. - */ - public TargetData(longchar target) - { - // TODO: better way to do this than overriding close()? Overriding flush() may cause to - // many assigns to the target longchar - stream = new ByteArrayOutputStream() - { - @Override - public void close() - throws IOException - { - flush(); - super.close(); - byte[] buf = toByteArray(); - target.assign(new String(buf)); - } - }; + origType = type; + origDst = new character(target); + } + + /** + * Configures the supported resource types for the current usage. The caller passes in its name and the + * method does the selection. It also does the verification whether the already configured parameters (in + * the constructor) are valid and eventual raise the error condition. + * + * @param method + * The identifier for the method using the source stream. It is used to get the set of acceptable + * stream types to check against them. + * @param widgetType + * The parent widget. Only used for composing the error messages. + * + * @return {@code true} if the parameters configured in constructor seems correct. This method does not + * check the content, only the variable types. If there is not a match with the possible streams + * supported by the {@code method}, {@code false} is returned, after eventually raising a specific + * error condition. + */ + public boolean configureSupportedTargets(String method, String widgetType) + { + boolean isJson = method.contains("JSON"); + if (origType == null) + { + ErrorManager.recordOrShowError(5442, method, "character"); + // Invalid datatype for argument to method ''. Expecting '' + return false; + } + + if (origDst == null || origDst.isUnknown()) + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + + // [method] must always be one of the supported methods + int supportedDestinations = supportedStreams.get(method); + + switch (origType.toUpperCase()) + { + case TYPE_FILE: + if ((supportedDestinations & SER_FILE) != 0) + { + if (!(origDst instanceof character)) + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + return true; + } + break; + + case TYPE_STREAM: + if ((supportedDestinations & SER_STREAM) != 0) + { + return true; + } + break; + + case TYPE_STREAM_HANDLE: + if ((supportedDestinations & SER_STREAM_HANDLE) != 0) + { + return true; + } + break; + + case TYPE_MEMPTR: + if ((supportedDestinations & SER_MEMPTR) != 0) + { + if (!(origDst instanceof memptr)) + { + break; + } + return true; + } + break; + + case TYPE_HANDLE: + if (!isJson) + { + ErrorManager.recordOrShowError(13186); + // Handle type not valid as WRITE-XML target. (13186) + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + break; + + case TYPE_LONGCHAR: + if ((supportedDestinations & SER_LONGCHAR) != 0) + { + if (!(origDst instanceof longchar)) + { + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + return false; + } + return true; + } + break; + + case TYPE_JSON_OBJECT: + if ((supportedDestinations & SER_JSON_OBJECT) != 0) + { + return true; + } + break; + + case TYPE_JSON_ARRAY: + if ((supportedDestinations & SER_JSON_ARRAY) != 0) + { + return true; + } + break; + } + + if (isJson) + { + ErrorManager.recordOrShowError(15344, origType, method); + // Invalid mode '' for . (15344) + } + else + { + ErrorManager.recordOrShowError(13185, origType); + // Invalid target-type for WRITE-XML: . (13185) + ErrorManager.recordOrShowError(4065, method, widgetType); + // **The attribute on the has invalid arguments. (4065) + } + return false; } /** * Get the output stream representing the target resource to which data will be written. * This may be a remote or a local resource. - * + * * @return Output stream. */ - public OutputStream getOutputStream() + @Override + public OutputStream getStream() { - return stream; + if (stream != null) + { + return stream; + } + + switch (origType.toUpperCase()) + { + case TYPE_JSON_ARRAY: + case TYPE_JSON_OBJECT: + UnimplementedFeature.todo("BUFFER:{SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray) is not implemented."); + return null; + + case TYPE_STREAM: + case TYPE_STREAM_HANDLE: + case TYPE_FILE: + if (origDst instanceof character) + { + fileName = ((character) origDst).toJavaType(); + stream = new OutputStreamWrapper(StreamFactory.openFileStream(fileName, true, false)); + return stream; + } + return null; + + case TYPE_LONGCHAR: + if (origDst instanceof longchar) + { + longchar dest = (longchar) origDst; + stream = new ByteArrayOutputStream() + { + @Override + public void close() + throws IOException + { + flush(); + super.close(); + dest.assign(this.toString()); + } + }; + return stream; + } + return null; + + case TYPE_MEMPTR: + if (origDst instanceof memptr) + { + memptr dest = (memptr) origDst; + stream = new ByteArrayOutputStream() + { + @Override + public void close() + throws IOException + { + flush(); + super.close(); + dest.assign(this.toByteArray()); + } + }; + return stream; + } + return null; + } + + return null; } /** * Close the output stream underlying this data source. - * + * * @throws IOException * if there is an error closing the stream. */ - void close() + public void close() throws IOException { - stream.close(); - } - - /** - * Initialize a remote file or stream resource as an output stream. - *

- * The stream resource is unimplemented currently. - * - * @param type - * Type of output resource: FILE or STREAM. - * @param name - * The name of the resource (path for a file; name for a stream). - */ - private void initializeRemote(String type, String name) - { - switch(type.toUpperCase()) - { - case "FILE": - stream = new OutputStreamWrapper(StreamFactory.openFileStream(name, true, false)); - break; - case "STREAM": - // TODO: get the stream and wrap it in OutputStreamWrapper - UnimplementedFeature.missing("TargetData STREAM support"); - break; - default: - throw new IllegalArgumentException("Data target type must be FILE or STREAM"); - } - } - - /** - * Initialize a remote stream handle or handle resource as an output stream. - *

- * Both types are unimplemented features currently. - * - * @param type - * Type of output resource: STREAM-HANDLE or HANDLE. - * @param target - * The handle to the resource. - */ - private void initializeRemote(String type, handle target) - { - switch(type.toUpperCase()) - { - case "STREAM-HANDLE": - UnimplementedFeature.missing("TargetData STREAM-HANDLE support"); - break; - case "HANDLE": - UnimplementedFeature.missing("TargetData HANDLE support"); - break; - default: - throw new IllegalArgumentException( - "Data target type must be STREAM-HANDLE or HANDLE"); + if (stream != null) + { + stream.close(); } } } === modified file 'src/com/goldencode/p2j/persist/TempRecord.java' --- src/com/goldencode/p2j/persist/TempRecord.java 2020-08-28 08:40:21 +0000 +++ src/com/goldencode/p2j/persist/TempRecord.java 2020-12-19 00:59:30 +0000 @@ -9,6 +9,7 @@ ** OM 20200110 Added constants and method implementations. ** CA 20200714 Refactored to keep the reserved properties in the record's data, to be persisted in the ** table. +** 002 OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -250,7 +251,7 @@ * @see #_ERROR_FLAG */ @Override - public final Integer _errorFlag() + public final Integer _errorFlags() { return (Integer) data[_ERROR_FLAG_DATA_INDEX]; } @@ -264,7 +265,7 @@ * @see #_ERROR_FLAG */ @Override - public final void _errorFlag(Integer flag) + public final void _errorFlags(Integer flag) { int savedActiveOffset = this.activeOffset; try === modified file 'src/com/goldencode/p2j/persist/TempTable.java' --- src/com/goldencode/p2j/persist/TempTable.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/TempTable.java 2021-01-15 23:08:08 +0000 @@ -2,7 +2,7 @@ ** Module : TempTable.java ** Abstract : Declaration of some dynamic TEMP-TABLES specific features (attributes, methods). ** -** Copyright (c) 2013-2020, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 OM 20130129 Created initial version. @@ -35,7 +35,9 @@ ** 023 OM 20191031 Removed method signatures pushed to super interface. ** 024 ECF 20200906 New ORM implementation. ** 025 AIL 20200911 Added Dereferenceable interface. -** 026 OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201120 Added declarations for SCHEMA-MARSHALL related accessors. +** OM 20210106 Fixed 2nd parameter of TEMP-TABLE-PREPARE. */ /* @@ -108,6 +110,8 @@ Clearable, CommonHandleChain, CreateLike, + DataSourceModifiable, + Deletable, Dereferenceable, DynamicResource, EmptyTempTable, @@ -116,10 +120,14 @@ InstantiatingProcedure, JsonData, Nameable, + NamedSerializable, + NamespaceURI, + Rejectable, TempTableDuplicator, UndoStateProvider, UniqueID, - XmlData + XmlData, + XmlNode { /** The type of this buffer from the {@code DataSet} changes point of view. */ enum BeforeType @@ -140,6 +148,20 @@ SIMPLE } + /** Constant for SCHEMA-MARSHAL attribute: the full schema is serialized with table parameter. */ + public static final String SCHEMA_MARSHAL_FULL = "FULL"; + + /** + * Constant for SCHEMA-MARSHAL attribute: minimal schema is marshalled. When in use, the field names, data + * types and extents, and temp-table ERROR-STRING are serialized with table parameter, but index + * descriptions and other field information (label, help, field validation expression, and so on (?)) are + * not. + */ + public static final String SCHEMA_MARSHAL_MIN = "MIN"; + + /** Constant for SCHEMA-MARSHAL attribute: no schema is marshalled (just table data). */ + public static final String SCHEMA_MARSHAL_NONE = "NONE"; + /** * Adds a field with the specified properties to the temp-table. * This method is the P2J equivalent of ADD-NEW-FIELD method @@ -149,7 +171,7 @@ * The name of the field to be created in the temp-table. * @param type * The data type of the specified field. - * + * * @return true on success. */ @LegacyMethod(name = "ADD-NEW-FIELD") @@ -166,12 +188,12 @@ * The data type of the specified field. * @param extent * An integer expression specifying the extent of an array. - * + * * @return true on success. */ @LegacyMethod(name = "ADD-NEW-FIELD") public logical addNewField(character name, character type, integer extent); - + /** * Adds a field with the specified properties to the temp-table. * This method is the P2J equivalent of ADD-NEW-FIELD method @@ -191,7 +213,7 @@ */ @LegacyMethod(name = "ADD-NEW-FIELD") public logical addNewField(character name, character type, integer extent, character format); - + /** * Adds a field with the specified properties to the temp-table. * This method is the P2J equivalent of ADD-NEW-FIELD method @@ -210,7 +232,7 @@ * An expression that evaluates to the initial value of the * defined field. * TODO: this method will probably be overloaded because of this. - * + * * @return true on success. */ @LegacyMethod(name = "ADD-NEW-FIELD") @@ -219,7 +241,7 @@ integer extent, character format, BaseDataType initial); - + /** * Adds a field with the specified properties to the temp-table. * This method is the P2J equivalent of ADD-NEW-FIELD method @@ -487,7 +509,7 @@ */ @LegacyMethod(name = "ADD-LIKE-INDEX") public logical addIndexLike(character name, character sourceName, handle sourceBuffer); - + /** * Signals that all the field and index definitions for a temp-table * have been provided. After the call to this method no fields and indexes @@ -503,7 +525,7 @@ */ @LegacyMethod(name = "TEMP-TABLE-PREPARE") public logical tempTablePrepare(String name); - + /** * Signals that all the field and index definitions for a temp-table have been provided. After * the call to this method no fields and indexes can be added to this temporary table. @@ -525,8 +547,7 @@ * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. * * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. + * Temp-table name to be used in subsequent query statements which refer to this temp-table. * @param before * Create the {@code BEFORE-TABLE} also if {@code true}. * @@ -541,8 +562,37 @@ * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. * * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @LegacyMethod(name = "TEMP-TABLE-PREPARE") + public logical tempTablePrepare(String name, logical before); + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @LegacyMethod(name = "TEMP-TABLE-PREPARE") + public logical tempTablePrepare(character name, boolean before); + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. * @param before * Create the {@code BEFORE-TABLE} also if {@code true}. * @@ -1062,4 +1112,86 @@ */ @LegacyAttribute(name = "ORIGIN-HANDLE") public handle getOriginHandle(); + + /** + * Implements the {@code NO-SCHEMA-MARSHAL} attribute getter. + * + * @return {@code true} when {@code SCHEMA-MARSHAL} attribute is set to {@code "NONE"}. + */ + @LegacyAttribute(name = "NO-SCHEMA-MARSHAL") + public logical isNoSchemaMarshal(); + + /** + * Sets the {@code SCHEMA-MARSHAL} attribute to {@code "NO"} value when {@code on = true} or + * back to default value {@code on = false}. + * + * @param on + * The new value of {@code SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "NO-SCHEMA-MARSHAL", setter = true) + public void setNoSchemaMarshal(boolean on); + + /** + * Sets the {@code SCHEMA-MARSHAL} attribute to {@code "NO"} value when {@code on = true} or + * back to default value {@code on = false}. + * + * @param on + * The new value of {@code SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "NO-SCHEMA-MARSHAL", setter = true) + public void setNoSchemaMarshal(logical on); + + /** + * Implements the {@code MIN-SCHEMA-MARSHAL} attribute getter. + * + * @return {@code true} when {@code SCHEMA-MARSHAL} attribute is set to {@code "MIN"}. + */ + @LegacyAttribute(name = "MIN-SCHEMA-MARSHAL") + public logical isMinSchemaMarshal(); + + /** + * Sets the {@code SCHEMA-MARSHAL} attribute to {@code "MIN"} value when {@code on = true} or + * back to default value {@code on = false}. + * + * @param on + * The new value of {@code MIN-SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "MIN-SCHEMA-MARSHAL", setter = true) + public void setMinSchemaMarshal(boolean on); + + /** + * Sets the {@code SCHEMA-MARSHAL} attribute to {@code "MIN"} value when {@code on = true} or + * back to default value {@code on = false}. + * + * @param on + * The new value of {@code MIN-SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "MIN-SCHEMA-MARSHAL", setter = true) + public void setMinSchemaMarshal(logical on); + + /** + * Obtain the current value of {@code SCHEMA-MARSHAL} attribute. + * + * @return the current value of {@code SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "SCHEMA-MARSHAL") + public character getSchemaMarshal(); + + /** + * Sets a new value for {@code SCHEMA-MARSHAL} attribute. + * + * @param level + * the new value or {@code SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "SCHEMA-MARSHAL", setter = true) + public void setSchemaMarshal(String level); + + /** + * Sets a new value for {@code SCHEMA-MARSHAL} attribute. + * + * @param level + * the new value or {@code SCHEMA-MARSHAL} attribute. + */ + @LegacyAttribute(name = "SCHEMA-MARSHAL", setter = true) + public void setSchemaMarshal(Text level); } === modified file 'src/com/goldencode/p2j/persist/TempTableBuilder.java' --- src/com/goldencode/p2j/persist/TempTableBuilder.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/TempTableBuilder.java 2021-01-21 22:49:45 +0000 @@ -3,7 +3,7 @@ ** Abstract : Implementation of the dynamic features for constructing temp-tables * (attributes, methods and constructors). ** -** Copyright (c) 2013-2020, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 OM 20130129 Created initial version. @@ -82,6 +82,14 @@ ** OM 20200924 Objects of this class carry multiple information to avoid map lookups for them. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** SVL 20201030 Reflect extension of PropertyDefinition. +** SVL 20201110 Manage BufferField handles in addFieldLike. +** OM 20201120 Renamed NAMESPACE-URI and NAMESPACE-PREFIX setters to fit buffer attributes naming +** convention. Allowed Object fields to be added to TEMP-TABLEs. Added CLEAR state +** awareness. Exposed addField() to XML deserializer. +** OM 20201202 Avoided conditions to be generated while encountered exeptions in debug mode. +** 20210106 Dropped 2nd parameter of TEMP-TABLE-PREPARE. +** OM 20210115 The before-table is automaticaly created when needed. */ /* @@ -233,8 +241,7 @@ ParmType.CHAR}) { String typeName = type.toString(); - defaultFormatStrings.put(typeName, - DisplayFormat.instanceOfType(typeName).defaultFormatString()); + defaultFormatStrings.put(typeName, DisplayFormat.instanceOfType(typeName).defaultFormatString()); } // separately because used "datetimetz" and "datetime-tz" defaultFormatStrings.put(ParmType.DTTZ.toString(), @@ -256,8 +263,6 @@ * * @param tableName * The table name. - * @param afterTable - * Flag indentifying if this is an after or before table. * @param props * The property definitions. * @param tableIndexes @@ -268,7 +273,6 @@ * The XML prefix. */ public static BufferImpl createRemoteTable(String tableName, - boolean afterTable, Iterator props, String tableIndexes, String xmlns, @@ -304,20 +308,20 @@ } } - ttb.tempTablePrepare(tableName, afterTable); + ttb.tempTablePrepare(tableName); BufferImpl buff = (BufferImpl) ttb.defaultBufferHandle().getResource(); if (xmlns != null) { - buff.setNamespaceURI(xmlns); + buff.namespaceURI(xmlns); } if (xmlPrefix != null) { - buff.setNamespacePrefix(xmlPrefix); + buff.namespacePrefix(xmlPrefix); } return buff; } - + /** * Dynamically creates a temporary table at runtime and assign it to a * handle. The table is empty and will be populated with fields, indexes @@ -446,7 +450,7 @@ } /** - * Get the field of an existing table. + * Get the field (legacy name) of an existing table. * * @param persistence * Persistence which corresponds the target database. @@ -455,22 +459,21 @@ * @param fieldName4GL * 4GL field name. * - * @return field with the specified name or null if there is no such field. + * @return field with the specified name or {@code null} if there is no such field. */ public static P2JField getExistingField(Persistence persistence, String tableName4GL, String fieldName4GL) { String normalizedTableName4GL = normalizeName(tableName4GL); String normalizedFieldName4GL = normalizeName(fieldName4GL); - TempTable tt = null; String propertyName; - Class dmoClass; + DmoMeta dmoInfo; Database database = persistence.getDatabase(); if (DatabaseManager.TEMP_TABLE_DB.equals(database)) { - tt = getExistingTempTable(normalizedTableName4GL); + TempTable tt = getExistingTempTable(normalizedTableName4GL); propertyName = TableMapper.getPropertyName(tt, normalizedFieldName4GL); - dmoClass = tt.getDMOClass(); + dmoInfo = tt.getDmoMeta(); } else { @@ -478,7 +481,7 @@ propertyName = TableMapper.getPropertyName(schema, normalizedTableName4GL, normalizedFieldName4GL); - dmoClass = TableMapper.getDMOClass(schema, normalizedTableName4GL); + dmoInfo = DmoMetadataManager.getDmoInfo(TableMapper.getDMOClass(schema, normalizedTableName4GL)); } if (propertyName == null) @@ -486,51 +489,49 @@ return null; } - return getExistingField(DmoMetadataManager.getDMOInterface(dmoClass), propertyName, tt); + return getExistingField(dmoInfo, propertyName); } - + /** * Get the field of an existing table. * * @param buffer * A record buffer which backs records of the target table. - * @param fieldNameHibernate - * Hibernate field name. + * @param propertyName + * The property name. * - * @return field with the specified name or null if there is no such field. + * @return field with the specified name or {@code null} if there is no such field. */ - public static P2JField getExistingField(RecordBuffer buffer, String fieldNameHibernate) + public static P2JField getExistingField(RecordBuffer buffer, String propertyName) { if (buffer.isDynamic()) { TempTableBuilder table = DynamicTablesHelper.getTableInfo(buffer); Class dmoIface = buffer.getDMOInterface(); - String fieldName4GL = DynamicTablesHelper.get4GLFieldName(dmoIface, fieldNameHibernate); + String fieldName4GL = DynamicTablesHelper.get4GLFieldName(dmoIface, propertyName); return table.getField(fieldName4GL); } else { TempTable tt = buffer.isTemporary() ? buffer.getParentTable() : null; - return getExistingField(buffer.getDMOInterface(), fieldNameHibernate, tt); + return getExistingField(tt.getDmoMeta(), propertyName); } } /** - * Get the field of an existing table. + * Get the field (legacy name) of an existing table. * - * @param dmoIface - * DMO interface. + * @param dmoInfo + * DMO metadata for the table. * @param propertyName * Property name of the field. - * @param tempTable - * Temp table object. {@code null} for permanent tables. * * @return field with the specified name or {@code null} if there is no such field. */ - public static P2JField getExistingField(Class dmoIface, - String propertyName, - TempTable tempTable) + public static P2JField getExistingField(DmoMeta dmoInfo, String propertyName) { + Class dmoIface = dmoInfo.getAnnotatedInterface(); + if (!PropertyHelper.isLoaded(dmoIface)) { // just create an instance and analyze it; @@ -539,7 +540,6 @@ Map getters = PropertyHelper.legacyGettersByProperty(dmoIface); Map extents = PropertyHelper.extentsByProperty(dmoIface); - DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface); // get legacy property name Property fieldInfo = dmoInfo.getFieldInfo(propertyName); @@ -815,30 +815,30 @@ public static Collection getExistingFields(Persistence persistence, String tableName4GL) { String normalizedTableName4GL = normalizeName(tableName4GL); - Database database = persistence.getDatabase(); - Class dmoClass; StaticTempTable staticTempTable = null; + DmoMeta dmoInfo = null; if (database.equals(DatabaseManager.TEMP_TABLE_DB)) { staticTempTable = (StaticTempTable) getExistingTempTable(normalizedTableName4GL); - dmoClass = staticTempTable.getDMOClass(); + dmoInfo = staticTempTable.getDmoMeta(); } else { String schema = DatabaseManager.getSchema(database); - dmoClass = TableMapper.getDMOClass(schema, normalizedTableName4GL); + dmoInfo = DmoMetadataManager.getDmoInfo(TableMapper.getDMOClass(schema, normalizedTableName4GL)); } - Class dmoIface = DmoMetadataManager.getDMOInterface(dmoClass); + // TODO: use [dmoInfo.getExistingFields()] instead, after adding field/property/column support in P2JField + Class dmoIface = dmoInfo.getAnnotatedInterface(); String[] properties = getPropertyNamesArray(dmoIface, staticTempTable); List res = new ArrayList<>(); for (String property : properties) { // TODO: the code below collects "legacy" fields only. Is this correct? - P2JField field = getExistingField(dmoIface, property, staticTempTable); + P2JField field = getExistingField(dmoInfo, property); res.add(field); } @@ -1048,11 +1048,15 @@ // get first word int spaceIdx = datatype.indexOf(' '); String compareStr = spaceIdx >= 0 ? datatype.substring(0, spaceIdx) : datatype; - + if (compareStr.length() > 0) { compareStr = compareStr.toLowerCase(); - + if ("progress.lang.object".equals(compareStr)) + { + compareStr = "object"; + } + for (String type : datatypesMatchLen.keySet()) { if (type.startsWith(compareStr)) @@ -1700,9 +1704,25 @@ // we have the field with the same name return new logical(false); } - + + RecordBuffer buffer; + String fieldNameHibernate; Object handleValue = source.get(); - if (!FieldReference.class.isAssignableFrom(handleValue.getClass())) + Class cls = handleValue.getClass(); + if (FieldReference.class.isAssignableFrom(cls)) + { + FieldReference ref = (FieldReference) handleValue; + buffer = ref.getParentBuffer(); + fieldNameHibernate = ref.getProperty(); + } + else if (BufferField.class.isAssignableFrom(cls)) + { + BufferFieldImpl field = (BufferFieldImpl) handleValue; + Buffer buf = field.bufferHandle().unwrapBuffer(); + buffer = RecordBuffer.get((DataModelObject) buf); + fieldNameHibernate = field.getProperty(); + } + else { ErrorManager.displayError(9056, "ADD-LIKE-FIELD requires valid BUFFER-FIELD object", @@ -1710,10 +1730,6 @@ return new logical(false); } - FieldReference ref = (FieldReference) handleValue; - RecordBuffer buffer = ref.getParentBuffer(); - String fieldNameHibernate = ref.getProperty(); - P2JField srcField = getExistingField(buffer, fieldNameHibernate); P2JField targetField = copyField(targetName, srcField); @@ -2184,6 +2200,22 @@ } /** + * Check whether this TEMP-TABLE is in CLEAR state: I.e. the temp-table is first created or immediately + * after the CLEAR() method is applied. The other two state of a temp-table are: UNPREPARED and PREPARED. + * The UNPREPARED state between the first definitional method has been applied and before the + * TEMP-TABLE-PREPARE() method is applied. + *

+ * This method is FWD-internal. The programmer can check whether the temp-table is in an UNPREPARED or + * PREPARED state by checking the PREPARED attribute, but the CLEAR state is transparent. + * + * @return {@code true} if this table is in CLEAR state. + */ + public boolean _clear() + { + return fields.size() == 0; + } + + /** * Checks whether the resource is dynamic or static. * * @return true if the resource is dynamic, false otherwise. @@ -2221,50 +2253,6 @@ } /** - * Signals that all the field and index definitions for a temp-table have been provided. After - * the call to this method no fields and indexes can be added to this temporary table. - * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. - * - * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. - * @param before - * Create the {@code BEFORE-TABLE} also if {@code true}. - * - * @return {@code true} on success. - */ - @Override - public logical tempTablePrepare(String name, boolean before) - { - return tempTablePrepare(new character(name), new logical(before)); - } - - /** - * Signals that all the field and index definitions for a temp-table have been provided. After - * the call to this method no fields and indexes can be added to this temporary table. - * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. - * - * @param name - * Temp-table name to be used in subsequent query statements which refer to this - * temp-table. - * @param before - * Create the {@code BEFORE-TABLE} also if {@code true}. - * - * @return {@code true} on success. - */ - @Override - public logical tempTablePrepare(character name, logical before) - { - String tableName = name.getValue(); - String beforeName = null; - if (before != null && !before.isUnknown() && before.getValue()) - { - beforeName = "BI" + tableName; - } - return tempTablePrepareImpl(tableName, beforeName); - } - - /** * The implementation of {@code TEMP-TABLE-PREPARE} method. This method is invoked from both * user API and also from {@code DataSet} when a DATASET is duplicated. * @@ -2377,36 +2365,7 @@ if (beforeName != null) { - // create the BEFORE-TABLE - handle btth = TypeFactory.handle(); - TempTableBuilder.create(btth); - BufferImpl srcB4Buff = (BufferImpl) defaultBufferHandle().getResource(); - - TempTableBuilder newBtt = (TempTableBuilder) btth.getResource(); - newBtt.addField(buildField(ReservedProperty._ERRORFLAG, ParmType.INT, new integer())); - newBtt.addField(buildField(ReservedProperty._ORIGINROWID, ParmType.ROWID, new rowid())); - newBtt.addField(buildField(ReservedProperty._ERRORSTRING, ParmType.CHAR, new character())); - newBtt.addField(buildField(ReservedProperty._PEERROWID, ParmType.ROWID, new rowid())); - newBtt.addField(buildField(ReservedProperty._ROWSTATE, ParmType.INT, new integer())); - - newBtt.createLike(new handle(srcB4Buff)); - - // leave only the rowState index - newBtt.indexes.clear(); - P2JIndex rowStateIndex = new P2JIndex(beforeName, "rowState", false); - rowStateIndex.addComponent(ReservedProperty._ROWSTATE.legacy); - newBtt.addIndex(rowStateIndex); - - res = newBtt.tempTablePrepareImpl(beforeName, null).booleanValue(); - - if (res) - { - // dynamically link the new before TempTable to this after TempTable - newBtt.beforeTableType = BeforeType.BEFORE; - newBtt.peerTable = this; - this.beforeTableType = BeforeType.AFTER; - this.peerTable = newBtt; - } + res = createBeforeTable(beforeName); } if (beforeTableType == BeforeType.UNKNOWN) @@ -2421,41 +2380,159 @@ return new logical(res); } - - /** - * Signals that all the field and index definitions for a temp-table - * have been provided. After the call to this method no fields and indexes - * can be added to this temporary table. - * This method is the P2J equivalent of TEMP-TABLE-PREPARE - * method of Progress 4GL. + + /** + * Creates the before table and link it to current table. + * + * @param beforeName + * The name of the before table to be build. + * + * @return {@code true} if operation is successful and {@code false} otherwise. + */ + boolean createBeforeTable(String beforeName) + { + boolean res; + // create the BEFORE-TABLE + handle btth = TypeFactory.handle(); + TempTableBuilder.create(btth); + BufferImpl srcB4Buff = (BufferImpl) defaultBufferHandle().getResource(); + + TempTableBuilder newBtt = (TempTableBuilder) btth.getResource(); + newBtt.addField(buildField(ReservedProperty._ERRORFLAG, ParmType.INT, new integer())); + newBtt.addField(buildField(ReservedProperty._ORIGINROWID, ParmType.ROWID, new rowid())); + newBtt.addField(buildField(ReservedProperty._ERRORSTRING, ParmType.CHAR, new character())); + newBtt.addField(buildField(ReservedProperty._PEERROWID, ParmType.ROWID, new rowid())); + newBtt.addField(buildField(ReservedProperty._ROWSTATE, ParmType.INT, new integer())); + + newBtt.createLike(new handle(srcB4Buff)); + + // leave only the rowState index + newBtt.indexes.clear(); + P2JIndex rowStateIndex = new P2JIndex(beforeName, "rowState", false); + rowStateIndex.addComponent(ReservedProperty._ROWSTATE.legacy); + newBtt.addIndex(rowStateIndex); + + res = newBtt.tempTablePrepareImpl(beforeName, null).booleanValue(); + + if (res) + { + // dynamically link the new before TempTable to this after TempTable + newBtt.beforeTableType = BeforeType.BEFORE; + newBtt.peerTable = this; + this.beforeTableType = BeforeType.AFTER; + this.peerTable = newBtt; + // then update the buffer types and link them using peerBuffer + srcB4Buff.computeBufferType(); + srcB4Buff.peerBuffer.computeBufferType(); + } + return res; + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After the call to + * this method no fields and indexes can be added to this temporary table.
+ * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. * * @param name - * Temp-table name to be used in subsequent query statements that - * refer to this temp-table. + * Temp-table name to be used in subsequent query statements that refer to this temp-table. * - * @return true on success. + * @return {@code true} on success. */ public logical tempTablePrepare(String name) { - return tempTablePrepare(new character(name), new logical(false)); + return tempTablePrepareImpl(name, null); } /** - * Signals that all the field and index definitions for a temp-table - * have been provided. After the call to this method no fields and indexes - * can be added to this temporary table. - * This method is the P2J equivalent of TEMP-TABLE-PREPARE - * method of Progress 4GL. - * + * Signals that all the field and index definitions for a temp-table have been provided. After the call to + * this method no fields and indexes can be added to this temporary table.
+ * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * * @param name - * Temp-table name to be used in subsequent query statements that - * refer to this temp-table. - * - * @return true on success. + * Temp-table name to be used in subsequent query statements that refer to this temp-table. + * + * @return {@code true} on success. */ public logical tempTablePrepare(character name) { - return tempTablePrepare(name, new logical(false)); + String tableName = name.toJavaType(); + return tempTablePrepareImpl(tableName, null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(String name, boolean before) + { + return tempTablePrepareImpl(name, (before && name != null) ? "BI" + name : null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(String name, logical before) + { + boolean boolBef = !before.isUnknown() && before.booleanValue(); + return tempTablePrepareImpl(name, (boolBef && name != null) ? "BI" + name : null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(character name, boolean before) + { + String tableName = name.toJavaType(); + return tempTablePrepareImpl(tableName, (before && tableName != null) ? "BI" + tableName : null); + } + + /** + * Signals that all the field and index definitions for a temp-table have been provided. After + * the call to this method no fields and indexes can be added to this temporary table. + * This method is the P2J equivalent of {@code TEMP-TABLE-PREPARE} method of Progress 4GL. + * + * @param name + * Temp-table name to be used in subsequent query statements which refer to this temp-table. + * @param before + * Create the {@code BEFORE-TABLE} also if {@code true}. + * + * @return {@code true} on success. + */ + @Override + public logical tempTablePrepare(character name, logical before) + { + boolean boolBef = !before.isUnknown() && before.booleanValue(); + String tableName = name.toJavaType(); + return tempTablePrepareImpl(tableName, (boolBef && tableName != null) ? "BI" + tableName : null); } /** @@ -2897,7 +2974,7 @@ @Override public Class getDMOClass() { - return DmoMetadataManager.getImplementingClass(dmoIface); + return dmoInfo.getImplementationClass(); } /** @@ -2997,18 +3074,15 @@ // ParmType has this encoded as FWD class names, I'm not changing it there as it might affect conversion String dataType = parmType == ParmType.DTTZ ? "datetime-tz" : parmType.toString(); - - if (propertyDefinition.isExtent()) - { - addNewField(new character(propertyDefinition.getLegacyName()), - new character(dataType), - new integer(propertyDefinition.getExtent())); - } - else - { - addNewField(new character(propertyDefinition.getLegacyName()), - new character(dataType)); - } + integer extent = propertyDefinition.isExtent() ? new integer(propertyDefinition.getExtent()) : null; + + addNewField(new character(propertyDefinition.getLegacyName()), + new character(dataType), + extent, + new character(propertyDefinition.getFormat()), + null, + new character(propertyDefinition.getLabel()), + new character(propertyDefinition.getColumnLabel())); } /** @@ -3535,13 +3609,16 @@ } /** - * Add new field to the table. Doesn't check whether the field with the same name already - * exists. + * Add new field to the table. Doesn't check whether the field with the same name already exists. + *

+ * Note: + * This method was made public in order to be directly accessed from + * {@link com.goldencode.p2j.persist.serial.XmlImport}. * * @param field * Field to add. */ - private void addField(P2JField field) + public void addField(P2JField field) { fields.put(normalizeName(field.getName()), field); } @@ -3635,7 +3712,28 @@ String myName = (name == null) ? "unnamed" : name().getValue(); longchar lc = new longchar(); - writeJson(new TargetData(lc), new logical(true)); + ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper(); + boolean ws = eh.isSilent(); + boolean wp = eh.isPending(); + if (!ws) + { + eh.setSilent(true); + } + try + { + writeJson(new TargetData(TargetData.TYPE_LONGCHAR, lc), new logical(true)); + } + finally + { + if (!ws) + { + eh.setSilent(false); + } + if (!wp && eh.isPending()) + { + eh.clearPending(); + } + } return "DYNAMIC " + myName + ", ID " + getUniqueID().getValue() + "\n" + lc.toStringMessage(); } } === modified file 'src/com/goldencode/p2j/persist/TempTableRecord.java' --- src/com/goldencode/p2j/persist/TempTableRecord.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/TempTableRecord.java 2021-01-13 21:04:41 +0000 @@ -4,12 +4,13 @@ ** ** Copyright (c) 2019-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 OM 20190604 Initial version. ** 002 OM 20190831 Added hidden fields and index specific to BEFORE TEMP-TABLES. ** 003 OM 20191014 Before-Buffer specific column constants were moved to Buffer interface. ** 004 CA 20191119 Added toArray(), which returns the meta information in an array form. ** 005 ECF 20200906 New ORM implementation. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -158,7 +159,7 @@ * * @see Buffer#__ERROR_FLAG__ */ - public Integer _errorFlag(); + public Integer _errorFlags(); /** * Sets the current value of the {@code __error-flag__} for this record. @@ -168,7 +169,7 @@ * * @see Buffer#__ERROR_FLAG__ */ - public void _errorFlag(Integer flag); + public void _errorFlags(Integer flag); /** * Gets the current value of the {@code __error-string__} for this record. @@ -202,7 +203,7 @@ this._rowState(), this._peerRowid(), this._originRowid(), - this._errorFlag(), + this._errorFlags(), this._errorString() }; } === modified file 'src/com/goldencode/p2j/persist/TempTableResultSet.java' --- src/com/goldencode/p2j/persist/TempTableResultSet.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/TempTableResultSet.java 2020-11-20 18:40:59 +0000 @@ -26,6 +26,7 @@ ** 014 CA 20200427 Flush the buffer before reading the records. ** 015 ECF 20200906 New ORM implementation. ** 016 OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** SVL 20201030 Reflect extension of PropertyDefinition. */ /* @@ -356,12 +357,23 @@ if (extent == 0) { props[dmoPropIndex.get(prop.name)] = - new PropertyDefinition(prop.name, prop._fwdType, prop.legacy); + new PropertyDefinition(prop.name, + prop._fwdType, + prop.legacy, + prop.format, + prop.label, + prop.columnLabel); } else { props[dmoPropIndex.get(prop.name)] = - new PropertyDefinition(prop.name, prop._fwdType, extent, prop.legacy); + new PropertyDefinition(prop.name, + prop._fwdType, + extent, + prop.legacy, + prop.format, + prop.label, + prop.columnLabel); } } === modified file 'src/com/goldencode/p2j/persist/TemporaryBuffer.java' --- src/com/goldencode/p2j/persist/TemporaryBuffer.java 2020-10-05 18:05:51 +0000 +++ src/com/goldencode/p2j/persist/TemporaryBuffer.java 2021-01-29 00:53:41 +0000 @@ -2,7 +2,7 @@ ** Module : TemporaryBuffer.java ** Abstract : Specialized record buffer which supports temp table semantics ** -** Copyright (c) 2004-2020, Golden Code Development Corporation. +** Copyright (c) 2004-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 ECF 20060125 @24356 Created initial version. Extends RecordBuffer @@ -441,8 +441,7 @@ ** DMO alias binding is limited only for the duration of that top-level ** block call. ** 152 IAS 20200413 Fixed DMO instantiation on SESSION:DATE-FORMAT. -** CA 20200427 Allow a TABLE to be read from JSON argument, in case of a remote REST -** call. +** CA 20200427 Allow a TABLE to be read from JSON argument, in case of a remote REST call. ** CA 20200514 Added support for SOAP web services (field are sent as BDT already, but the ** 'fromJson' flags remains). ** 153 ECF 20200419 Removed Hibernate dependencies. @@ -468,6 +467,20 @@ ** Use an identity HashSet where possible. ** CA 20201005 Fixed bulkCopyAllRows and copyAllRows - H2 will now match the WHERE and ORDER BY ** indexes, avoiding re-sorting the fetched rows. +** AIL 20201031 Added sorted and direct keywords for insert into select from. +** AIL 20201118 Made extent fields bulk copy use insert into select from. +** 20201125 Added fast copy validation using DMO signature. +** 20201201 Moved fast copy implementation to FastCopyHelper. Added helper methods. +** CA 20201202 Record change notifications are processed only if the multiplex ID matches with +** the event's source. +** AIL 20201215 Added before table prop mapping to the fast-copy helper call. +** 20201216 Added support for fast loose-copy. +** 20201228 Invalidated fast-find cache after fast-copy. +** CA 20201118 Fixed a leak related to cleaning up the multiplex scope when deleting a static +** temp-table part of a persistent external program. +** OM 20201120 Fixed buffer copy methods. Fixed rejectChanges() method. Fixed management of +** BEFORE-BUFFER atttibutes. Fixed buffer/table copy. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -691,11 +704,11 @@ /** The actual proxy instance wrapping the DMO proxy. */ private Temporary mutableProxy; - /** + /** * For a master buffer, all explicit buffers created for it. * TODO: I think there is a memory leak hear, needs to be reviewed. */ - private Set explicitBuffers = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set explicitBuffers = Collections.newSetFromMap(new IdentityHashMap<>()); /** * Constructor. This class is only instantiated via the {@link #define} @@ -2757,7 +2770,7 @@ dstRec._rowState((Integer) rowMeta[1]); dstRec._peerRowid((Long) rowMeta[2]); // this will be processed later on // TODO: originRowId - does it have any meaning? - dstRec._errorFlag((Integer) rowMeta[4]); + dstRec._errorFlags((Integer) rowMeta[4]); dstRec._errorString((String) rowMeta[5]); savedAny = true; @@ -3157,48 +3170,49 @@ } /** - * Copy all records from one temp table to another, optionally appending to records already - * present in the destination table. + * Copy all records from one temp table to another, optionally appending to records already present in the + * destination table. * * @param srcRecId - * The {@link rowid} of the current record in the source buffer - if not null, it will - * be set in the destination buffer, too. + * The {@link rowid} of the current record in the source buffer - if not null, it will be set in + * the destination buffer, too. * @param srcDMO - * DMO instance returned by a previous call to {@link #define} on the source - * temporary buffer. + * DMO instance returned by a previous call to {@link #define} on the source temporary buffer. * @param dstDMO - * DMO instance returned by a previous call to {@link #define} on the - * destination temporary buffer. + * DMO instance returned by a previous call to {@link #define} on the destination temporary + * buffer. * @param append - * true to add new records without affecting existing records; - * false to remove all existing records before the copy takes place. + * {@code true} to add new records without affecting existing records; + * {@code false} to remove all existing records before the copy takes place. * @param errorMode * Table copy mode, affects error handling. * @param skipUniqueConflicts - * Corresponds append-mode in COPY-TEMP-TABLE. If there is a unique index on the - * target temp-table and a row with a duplicate key is found, the row is not replaced. - * This parameter is ignored if replacePrimaryRecords is - * true. - * @param replacePrimaryRecords - * TODO implement support - * Corresponds replace-mode in COPY-TEMP-TABLE. When true records in the - * target temp-table are replaced with corresponding records from the source - * temp-table. The target temp-table must have a unique primary index that be can used - * to find the corresponding record. When the corresponding record is found in the - * target temp-table, the target record is replaced with the source record. + * Corresponds append-mode in COPY-TEMP-TABLE. If there is a unique index on the target temp-table + * and a row with a duplicate key is found, the row is not replaced. This parameter is ignored if + * {@code replaceMode} is {@code true}. + * @param replaceMode + * Corresponds replace-mode in COPY-TEMP-TABLE. When {@code true} records in the target temp-table + * are replaced with corresponding records from the source temp-table. The target temp-table must + * have a unique primary index that be can used to find the corresponding record. When the + * corresponding record is found in the target temp-table, the target record is replaced with the + * source record. * @param looseCopy - * Corresponds loose-copy-mode in COPY-TEMP-TABLE. Only the fields with the same name - * that appear in both the source and target temp-table are copied. Other fields are - * ignored. + * Corresponds loose-copy-mode in COPY-TEMP-TABLE. Only the fields with the same name that appear + * in both the source and target temp-table are copied. Other fields are ignored. + *

Note:
+ * Because of an undocumented bug in OE, if the {@code replaceMode} is {@code true} the value will + * 'leak' and overwrite this parameter. As result, a temp-table can be copied to another, + * incompatible temp-table even if {@code looseCopy} is explicitly set to {@code false}, just by + * passing {@code true} for the previous parameter. * - * @return true if copy was successful. false if validation error - * has occurred or the table is appended to itself. + * @return {@code true} if copy was successful. + * {@code false} if validation error has occurred or the table is appended to itself. * * @throws ErrorConditionException * if a recoverable error occurs copying records. * @throws RuntimeException - * if an unrecoverable error occurs invoking methods on the underlying DMOs to get or - * set data values. + * if an unrecoverable error occurs invoking methods on the underlying DMOs to get or set data + * values. */ static boolean copyAllRows(rowid srcRecId, Temporary srcDMO, @@ -3206,10 +3220,16 @@ boolean append, CopyTableMode errorMode, boolean skipUniqueConflicts, - boolean replacePrimaryRecords, + boolean replaceMode, boolean looseCopy) { - // Get source and destination buffers, both of which must be TemporaryBuffer instances. + if (replaceMode) + { + // see the javadoc note above for [looseCopy] parameter + looseCopy = true; + } + + // get source and destination buffers, both of which must be TemporaryBuffer instances TemporaryBuffer srcBuf = (TemporaryBuffer) ((BufferReference) srcDMO).buffer(); TemporaryBuffer dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).buffer(); @@ -3222,10 +3242,12 @@ TemporaryBuffer dstB4Buf = null; boolean before = false; - if (((BufferImpl) srcDMO).isAfterBuffer() && ((BufferImpl) dstDMO).isAfterBuffer()) + BufferImpl dstBufImpl = (BufferImpl) dstDMO; + BufferImpl srcBufImpl = (BufferImpl) srcDMO; + if (srcBufImpl.isAfterBuffer() && dstBufImpl.isAfterBuffer()) { - srcB4DMO = (BufferImpl) ((BufferImpl) srcDMO).beforeBuffer().getResource(); - dstB4DMO = (BufferImpl) ((BufferImpl) dstDMO).beforeBuffer().getResource(); + srcB4DMO = (BufferImpl) srcBufImpl.beforeBuffer().getResource(); + dstB4DMO = (BufferImpl) dstBufImpl.beforeBuffer().getResource(); srcB4Buf = (TemporaryBuffer) ((BufferReference) srcB4DMO).buffer(); dstB4Buf = (TemporaryBuffer) ((BufferReference) dstB4DMO).buffer(); @@ -3279,8 +3301,8 @@ { if (errorMode == CopyTableMode.COPY_TEMP_TABLE_MODE) { - String msg = "Source and target are the same table in temp-table copy " + - "operation for " + dstBufName; + String msg = "Source and target are the same table in temp-table copy operation for " + + dstBufName; ErrorManager.displayError(12301, msg, false); return false; @@ -3299,12 +3321,10 @@ boolean simpleCopy = srcBuf.getDMOImplementationClass().equals(dstBuf.getDMOImplementationClass()); - // PROGRESS does not care if the field names are different; what it - // cares about are only the field type and position - if a source field - // has the same position and type as the destination field, then they - // are a match. If the source and destination fields at a certain - // position do not match, then PROGRESS will throw an error and abend - // the application. + // PROGRESS does not care if the field names are different; what it cares about are only the field type + // and position - if a source field has the same position and type as the destination field, then they + // are a match. If the source and destination fields at a certain position do not match, then PROGRESS + // will throw an error and abend the application. // if the backing tables are different, check if the fields match Map propsMap = null; @@ -3313,7 +3333,7 @@ if (!simpleCopy) { // we need to modify the setter properties, so instantiate a new map - propsMap = createPropsMap(looseCopy, (BufferImpl) srcDMO, (BufferImpl) dstDMO, errorMode); + propsMap = createPropsMap(looseCopy, srcBufImpl, dstBufImpl, errorMode); if (propsMap == null) { return false; @@ -3366,122 +3386,79 @@ if (!srcBuf.isTableDefinitelyEmpty()) { - DmoMeta dstMeta = dstBuf.getDmoInfo(); - - // skip validation when: - // 1. same table and not in append mode (as dst will be empty) or - // 2. there are no unique constraints or mandatory fields in dst table - // TODO: enhance this if we are not in append mode, the dst and src tables are different, and they - // have the same set of unique indexes (by the field position, not name) and the same set of - // mandatory fields (by field position, not name) - boolean bulkInsert = !append && - ((dstBuf.getDMOImplementationClass() == srcBuf.getDMOImplementationClass()) || - (dstMeta.getUniqueConstraints().isEmpty() && dstMeta.getMandatoryFields().isEmpty())); - - if (bulkInsert) - { - boolean hasBeforeRecords = before && !srcB4Buf.isTableDefinitelyEmpty(); - String srcSqlIndexName = srcBuf.local.getImplicitSqlIndexName(srcBuf.getDMOInterface()); - String srcIndexName = srcBuf.local.getImplicitIndexName(srcBuf.getDMOInterface()); - - Map srcToDstPks = bulkCopyAllRows(simpleCopy, - srcBuf, - dstBuf, - hasBeforeRecords || srcRecId != null, - propsMap, - srcSqlIndexName, - srcIndexName); - if (hasBeforeRecords) - { - String srcB4SqlIndexName = srcB4Buf.local.getImplicitSqlIndexName(srcB4Buf.getDMOInterface()); - String srcB4IndexName = srcB4Buf.local.getImplicitIndexName(srcB4Buf.getDMOInterface()); - Map srcB4ToDstB4Pks = bulkCopyAllRows(simpleCopy, - srcB4Buf, - dstB4Buf, - true, - propsMap, - srcB4SqlIndexName, - srcB4IndexName); - - DmoMeta dstB4Meta = dstB4Buf.getDmoInfo(); - String dstTableB4 = dstB4Meta.getSqlTableName(); - - List sqls = new ArrayList<>(); - for (Map.Entry entries : srcToDstPks.entrySet()) - { - Long srcPk = entries.getKey(); - Long[] pair = entries.getValue(); - if (pair[1] == null) - { - continue; - } - - Long dstPk = pair[0]; - Long srcPeerId = pair[1]; - Long dstB4Pk = srcB4ToDstB4Pks.get(srcPeerId)[0]; - - sqls.add("update " + dstTableB4 + - " set " + TempRecord._PEER_ROWID + " = " + dstPk + - " where " + Session.PK + " = " + dstB4Pk); - } - - String dstTable = dstMeta.getSqlTableName(); - for (Map.Entry entries : srcB4ToDstB4Pks.entrySet()) - { - Long srcB4Pk = entries.getKey(); - Long[] pair = entries.getValue(); - if (pair[1] == null) - { - continue; - } - - Long dstB4Pk = pair[0]; - Long srcB4PeerId = pair[1]; - Long dstPk = srcToDstPks.get(srcB4PeerId)[0]; - - sqls.add("update " + dstTable + - " set " + TempRecord._PEER_ROWID + " = " + dstB4Pk + - " where " + Session.PK + " = " + dstPk); - } - - persistence.executeSQLBatch(sqls, true); - } - - // at this point, we are sure the source table is not empty and all records were copied. - savedAny = true; - - if (srcRecId != null) - { - destRecId = new rowid(srcToDstPks.get(srcRecId.getValue())[0]); - } - - return true; + String replaceQuery = null; + if (replaceMode) + { + P2JIndex primaryIndex = dstBuf.dmoInfo.getPrimaryIndex(true); + if (primaryIndex == null || !primaryIndex.isUnique()) + { + ErrorManager.recordOrShowError(11885); + // A unique primary index is required in the target table, each field of which is mapped to some source field, in order to do a replace-mode or parent-mode COPY/GET/MERGE/FILL type of operation on a dataset table. + return false; + } + + StringBuilder sb = new StringBuilder("WHERE "); + Iterator it = primaryIndex.components(); + boolean first = true; + while (it.hasNext()) + { + if (first) + { + first = false; + } + else + { + sb.append(" and "); + } + + P2JIndexComponent comp = it.next(); + sb.append(dstBuf.getLegacyName()).append(".").append(comp.getLegacyName()).append("=") + .append(srcBuf.getLegacyName()).append(".").append(comp.getLegacyName()); + } + + replaceQuery = sb.toString(); + } + else + { + FastCopyHelper fastCopyHelper = new FastCopyHelper(simpleCopy, + srcBuf, + dstBuf, + propsMap, + propsB4Map, + srcB4Buf, + dstB4Buf); + if (fastCopyHelper.canFastCopy(looseCopy)) + { + destRecId = fastCopyHelper.executeCopy(srcRecId, append); + savedAny = true; + return true; + } } // get the list of source records to be copied. These will be all records in // srcBuf's backing temp table which have srcDMO's multiplex ID. - HQLExpression buf = new HQLExpression("from "); - buf.append(srcBuf.getDMOImplementationName()); + HQLExpression fqlBuilder = new HQLExpression("from "); + fqlBuilder.append(srcBuf.getDMOImplementationName()); // srcBuf is a TemporaryBuffer, so it isMultiplexed() - buf.append(" where "); - buf.append(MULTIPLEX_FIELD_NAME); - buf.append(" = ", true); + fqlBuilder.append(" where "); + fqlBuilder.append(MULTIPLEX_FIELD_NAME); + fqlBuilder.append(" = ", true); // force the records to be ordered by their ID - // TODO in copy-temp-table sorting order affects which record remains in constraints - // violation case. The correct sorting is by primary index if it presents or by - // the index with the lexicographically first name. - buf.append(" order by "); - buf.append(MULTIPLEX_FIELD_NAME); - buf.append(","); - buf.append(Session.PK); + // TODO: in copy-temp-table sorting order affects which record remains in constraints + // violation case. The correct sorting is by primary index if it presents or by + // the index with the lexicographically first name. + fqlBuilder.append(" order by "); + fqlBuilder.append(MULTIPLEX_FIELD_NAME); + fqlBuilder.append(","); + fqlBuilder.append(Session.PK); - String fql = buf.toFinalExpression(); + String fql = fqlBuilder.toFinalExpression(); String[] entities = new String[] { srcBuf.getEntityName() }; Object[] parms = new Object[] { srcBuf.getMultiplexID() }; ScrollableResults results = - persistence.scroll(entities, fql, parms, 0, 0, ResultSet.TYPE_FORWARD_ONLY); + persistence.scroll(entities, fql, parms, 0, 0, ResultSet.TYPE_FORWARD_ONLY); BufferManager bufMgr = srcBuf.getBufferManager(); @@ -3492,8 +3469,30 @@ // copy each source record into a new destination DMO instance Object[] row = results.get(); TempRecord srcRec = (TempRecord) row[0]; - TempRecord dstRec = dstBuf.instantiateDMO(); - dstRec.initialize(dstBuf.getSession(), false); + srcBuf.loadRecord(srcRec); + TempRecord dstRec = null; + boolean createNew = true; + + if (replaceMode) + { + logical found = dstBufImpl._find(replaceQuery, + BufferImpl.FindMode.UNIQUE, + LockType.EXCLUSIVE, + srcBufImpl); + if (found.booleanValue()) + { + // do not create a new record. An existing record matching the index was found and + // loaded into buffer + createNew = false; + dstRec = ((TempRecord) dstBufImpl.buffer().getCurrentRecord()); + } + } + + if (createNew) + { + dstRec = dstBuf.instantiateDMO(); + dstRec.initialize(dstBuf.getSession(), true); + } if (simpleCopy) { @@ -3520,7 +3519,6 @@ dstRec.primaryKey(id); // validate a record against the target table - // TODO also validate against already copied records try { dstBuf.validate(dstRec); @@ -3579,6 +3577,11 @@ savedAny = true; } } + catch (Throwable t) // debug block + { + t.printStackTrace(); + throw t; + } finally { if (results != null) @@ -3755,10 +3758,10 @@ TempTable srcTempTable = srcBuf.getParentTable(); TempTable dstTempTable = dstBuf.getParentTable(); - + String srcBufName = srcDMO.name().getValue(); String dstBufName = dstDMO.name().getValue(); - + String[] srcProperties = TempTableBuilder.getOrderedPropertyNames(srcTempTable.getDMOInterface(), srcTempTable); String[] dstProperties = TempTableBuilder.getOrderedPropertyNames(dstTempTable.getDMOInterface(), @@ -3808,13 +3811,17 @@ } dstPropName = dstProperties[idx]; } - else + else if (i < dstProperties.length) { dstPropName = dstProperties[i]; } + else + { + handleTablesDoNotMatch(errorMode, srcBufName, dstBufName); + return null; + } String srcPropName = srcProperties[i]; - DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcPropName); DatumAccess dstDatum = dstBuf.getSetterDatums().get(dstPropName); @@ -3896,287 +3903,6 @@ } /** - * Performance improved variant for {@link #copyAllRows}, when we know the destination table can have its - * records inserted via INSERT INTO ... SELECT FROM, without having to do any validation on - * the records, as the destination table will accept them all. - * - * @param simpleCopy - * Flag identifying if the source and destination tables have the same structure. - * @param srcBuf - * The source buffer. - * @param dstBuf - * The destination buffer. - * @param mapPks - * Flag indicating if the source and destination primary keys (and the after-rowid, - * must be computed mapped. Required to cross-reference before-table records or to place the - * destination buffer on the proper record, if the source buffer has a record loaded. - * @param propsMap - * The property mapping. - * @param sqlIndexName - * The SQL index name used in SELECT. - * @param indexName - * The index name to compute the ORDER BY clause. - * - * @return If mapPKs is true, it will return a mapping of source primary keys to the pair - * of destination primary key and the source record's after-rowid. - * - * @throws PersistenceException - * In case of problems during insert/select/etc. - */ - private static Map bulkCopyAllRows(boolean simpleCopy, - TemporaryBuffer srcBuf, - TemporaryBuffer dstBuf, - boolean mapPks, - Map propsMap, - String sqlIndexName, - String indexName) - throws PersistenceException - { - Persistence persistence = srcBuf.getPersistence(); - - DmoMeta srcMeta = srcBuf.getDmoInfo(); - DmoMeta dstMeta = dstBuf.getDmoInfo(); - - String sequenceName = dstBuf.local.sequenceName; - String srcTable = srcMeta.getSqlTableName(); - String dstTable = dstMeta.getSqlTableName(); - - StringBuilder columnListSrc = new StringBuilder("nextval('").append(sequenceName).append("'),") - .append(dstBuf.getMultiplexID()).append(",") - .append(ReservedProperty._ERRORFLAG.column).append(",") - .append(ReservedProperty._ORIGINROWID.column).append(",") - .append(ReservedProperty._ERRORSTRING.column).append(",") - .append(ReservedProperty._PEERROWID.column).append(",") - .append(ReservedProperty._ROWSTATE.column); - StringBuilder columnListDst = new StringBuilder(Session.PK).append(",") - .append(MULTIPLEX_FIELD_NAME).append(",") - .append(ReservedProperty._ERRORFLAG.column).append(",") - .append(ReservedProperty._ORIGINROWID.column).append(",") - .append(ReservedProperty._ERRORSTRING.column).append(",") - .append(ReservedProperty._PEERROWID.column).append(",") - .append(ReservedProperty._ROWSTATE.column); - - Map> extentFields = new HashMap<>(); - if (!simpleCopy) - { - for (Map.Entry prop : propsMap.entrySet()) - { - DatumAccess src = prop.getKey(); - DatumAccess dst = prop.getValue(); - Property srcProp = srcMeta.getFieldInfo(src.getPropertyName()); - Property dstProp = dstMeta.getFieldInfo(dst.getPropertyName()); - - if (srcProp.extent == 0) - { - columnListSrc.append(",").append(srcProp.column); - columnListDst.append(",").append(dstProp.column); - if (srcProp._isDatetimeTz) - { - // adding support for timezone offset, in case of datetime-tz data - columnListSrc.append(",").append(srcProp.column).append(DDLGeneratorWorker.DTZ_OFFSET); - columnListDst.append(",").append(dstProp.column).append(DDLGeneratorWorker.DTZ_OFFSET); - } - } - else - { - List extents = - extentFields.computeIfAbsent(srcProp.extent, k -> new ArrayList<>()); - extents.add(new Property[] { srcProp, dstProp }); - } - } - } - else - { - Iterator iter = dstMeta.getFields(false); - while (iter.hasNext()) - { - Property prop = iter.next(); - if (prop.extent == 0) - { - columnListSrc.append(",").append(prop.column); - columnListDst.append(",").append(prop.column); - if (prop._isDatetimeTz) - { - // adding support for timezone offset, in case of datetime-tz data - columnListSrc.append(",").append(prop.column).append(DDLGeneratorWorker.DTZ_OFFSET); - columnListDst.append(",").append(prop.column).append(DDLGeneratorWorker.DTZ_OFFSET); - } - } - else - { - List extents = - extentFields.computeIfAbsent(prop.extent, k -> new ArrayList<>()); - extents.add(new Property[] { prop, prop }); - } - } - } - - StringBuilder sort = null; - if (indexName != null) - { - Iterator iter = srcBuf.dmoInfo.getDatabaseIndexes(); - while (iter.hasNext()) - { - P2JIndex idx = iter.next(); - if (idx.getName().equals(indexName)) - { - sort = new StringBuilder(MULTIPLEX_FIELD_NAME); - Iterator iterc = idx.components(); - while (iterc.hasNext()) - { - P2JIndexComponent idxComp = iterc.next(); - String field = idxComp.getColumnName(); - if (idxComp.isCharType()) - { - // temp-tables always have computed columns - field = srcBuf.getDialect().getComputedColumnPrefix(!idxComp.isIgnoreCase()) + field; - } - sort.append(",").append(field).append(srcBuf.getDialect().orderByNulls(true)); - } - if (!idx.isUnique()) - { - sort.append(",").append(Session.PK); - } - break; - } - } - - if (sort == null) - { - throw new PersistenceException("Could not find index for " + indexName); - } - } - if (sort == null) - { - sort = new StringBuilder(MULTIPLEX_FIELD_NAME).append(",").append(Session.PK); - } - - Object[] multiplexArg = new Object[] { srcBuf.getMultiplexID() }; - Long firstDstPK = dstBuf.local.getCurrentPrimaryKey(dstBuf.getDMOInterface()) + 1; - persistence.executeSQL("alter sequence " + sequenceName + " restart with " + firstDstPK); - String sqlInsert = "insert into " + dstTable + " (" + columnListDst.toString() + ")" + - " select " + columnListSrc.toString() + - " from " + srcTable + - " use index (" + sqlIndexName + ")" + - " where " + MULTIPLEX_FIELD_NAME + " = ?" + - " order by " + sort.toString(); - - int recNum = persistence.executeSQL(sqlInsert, multiplexArg); - - Map srcToDstPks = null; - if (mapPks) - { - srcToDstPks = new HashMap<>(); - - // create the mappings of original PK to new PKs - String sqlPk = "select " + Session.PK + "," + TempRecord._PEER_ROWID + - " from " + srcTable + - " use index (" + sqlIndexName + ")" + - " where " + MULTIPLEX_FIELD_NAME + " = ?" + - " order by " + sort; - ScrollableResults srcPks = persistence.executeSQLQuery(sqlPk, multiplexArg); - int i = 0; - while (srcPks.next()) - { - Long srcPk = srcPks.get(0, Long.class); - Long srcPeerId = srcPks.get(1, Long.class); - Long dstPk = firstDstPK + i; - i = i + 1; - - srcToDstPks.put(srcPk, new Long[] { dstPk, srcPeerId }); - } - } - - // update the DMO's primary key generator - dstBuf.local.setCurrentPrimaryKey(dstBuf.getDMOInterface(), firstDstPK + recNum - 1); - - if (!extentFields.isEmpty()) - { - // insert records in all extent tables - for (Map.Entry> extent : extentFields.entrySet()) - { - // the SELECT (part of the INSERT) will not be able to use and INDEX, so we need to fetch all - // records and use INSERT with multiple rows (plus batch inserts). - - // the PARENT__ID will be computed explicitly - StringBuilder extColumnListSrc = new StringBuilder(ReservedProperty.LIST__INDEX.column); - StringBuilder extColumnListDst = new StringBuilder(); - extColumnListDst.append(ReservedProperty.PARENT__ID.column).append(",") - .append(ReservedProperty.LIST__INDEX.column); - StringBuilder values = new StringBuilder("?, ?"); // PARENT__ID, LIST__INDEX - for (Property[] props : extent.getValue()) - { - values.append(", ?"); - - extColumnListSrc.append(",").append(props[0].column); - if (props[0]._isDatetimeTz) - { - // adding support for timezone offset, in case of datetime-tz data - extColumnListSrc.append(",").append(props[0].column).append(DDLGeneratorWorker.DTZ_OFFSET); - } - - extColumnListDst.append(",").append(props[1].column); - if (props[1]._isDatetimeTz) - { - // adding support for timezone offset, in case of datetime-tz data - extColumnListDst.append(",").append(props[1].column).append(DDLGeneratorWorker.DTZ_OFFSET); - } - } - - String sqlExtSelect = - " select " + extColumnListSrc.toString() + - " from " + srcTable + "__" + extent.getKey() + - " join " + srcTable + " on " + ReservedProperty.PARENT__ID.column + " = " + Session.PK + - " where " + MULTIPLEX_FIELD_NAME + " = ?" + - " order by " + sort + ", " + - ReservedProperty.PARENT__ID.column + ", " + - ReservedProperty.LIST__INDEX.column; - ScrollableResults results = persistence.executeSQLQuery(sqlExtSelect, multiplexArg); - - StringBuilder sqlExtInsert = new StringBuilder(); - sqlExtInsert.append("insert into ").append(dstTable).append("__").append(extent.getKey()); - sqlExtInsert.append("(").append(extColumnListDst).append(") values "); - for (int i = 0; i < extent.getKey(); i++) - { - sqlExtInsert.append(i == 0 ? "" : ",").append("(").append(values).append(")"); - } - int width = 2 + extent.getValue().size(); - long[] nextRecPK = new long[] { firstDstPK }; - Object[] args = new Object[width * extent.getKey()]; - - Supplier applyArgs = () -> - { - for (int i = 0; i < extent.getKey(); i++) - { - if (!results.next()) - { - return null; - } - - Object[] next = results.get(); - int offset = i * width; - args[offset] = nextRecPK[0]; - - for (int k = 0; k < next.length; k++) - { - offset = offset + 1; - args[offset] = next[k]; - } - } - - nextRecPK[0]++; - - return args; - }; - - persistence.executeSQLBatch(sqlExtInsert.toString(), applyArgs); - } - } - - return srcToDstPks; - } - - /** * This is the actual implementation of TEMP-TABLE buffer's GET-CHANGES method. No validation * is performed, the method expects the parameters to be validated in calling method. *

@@ -4226,8 +3952,7 @@ // TODO in copy-temp-table sorting order affects which record remains in constraints // violation case. The correct sorting is by primary index if it presents or by // the index with the lexicographically first name. - beforeHql.append(" order by "); - beforeHql.append(Session.PK); + beforeHql.append(" order by ").append(Buffer.ROW_STATE_FIELD).append(",").append(Session.PK); ScrollableResults beforeRes = persistence.scroll(new String[] { srcBefore.getEntityName() }, @@ -4246,10 +3971,7 @@ TempRecord dstBeforeRec = dstBefore.instantiateDMO(); dstBeforeRec.initialize(dstBefore.getSession(), false); - copyDMO(dstBeforeRec, - srcBeforeRec, - (BufferImpl) dstBefore.getDMOProxy(), - (BufferImpl) srcBefore.getDMOProxy()); + BaseRecord.copy(dstBeforeRec, srcBeforeRec, true); Integer rowState = srcBeforeRec._rowState(); dstBeforeRec._rowState(rowState); @@ -4266,10 +3988,7 @@ { TempRecord dstAfterRec = dstAfter.instantiateDMO(); dstAfterRec.initialize(dstAfter.getSession(), false); - copyDMO(dstAfterRec, - srcAfterRec, - (BufferImpl) dstBefore.getDMOProxy(), - (BufferImpl) srcBefore.getDMOProxy()); + BaseRecord.copy(dstAfterRec, srcAfterRec, true); dstAfterRec._rowState(rowState); Long idChAfter = dstAfter.nextPrimaryKey(); @@ -4361,14 +4080,12 @@ /** * Rejects changes to the data in the AFTER-BUFFER of this BEFORE-BUFFER. * - * @param before - * The BEFORE-BUFFER to be processed. * @param after * The AFTER-BUFFER to be processed. * * @return {@code true} if operation is successful. */ - protected boolean rejectChanges(BufferImpl before, BufferImpl after) + protected boolean rejectChanges(BufferImpl after) { if (isTableDefinitelyEmpty()) { @@ -4412,25 +4129,29 @@ // restore back the deleted record: after.create(); Record deletedRec = afterDMO.getCurrentRecord(); - copyDMO(deletedRec, srcBeforeRec, after, before); + BaseRecord.copy(deletedRec, srcBeforeRec, false); Long idAfter = afterDMO.nextPrimaryKey(); - persistence.save(deletedRec, idAfter); + afterDMO.rowState(Buffer.ROW_UNMODIFIED); + afterDMO.originRowid(new rowid()); + after.release(); savedAny = true; break; + case Buffer.ROW_MODIFIED: // restore old value of fields after.findByRowID(new rowid(afterRowid)); if (after._available()) { Record afterRec = afterDMO.getCurrentRecord(); - copyDMO(afterRec, srcBeforeRec, after, before); + BaseRecord.copy(afterRec, srcBeforeRec, false); afterDMO.peerRowid(new rowid()); afterDMO.rowState(Buffer.ROW_UNMODIFIED); + afterDMO.originRowid(new rowid()); after.release(); savedAny = true; } + break; - break; case Buffer.ROW_CREATED: // delete back this record after.findByRowID(new rowid(afterRowid)); @@ -4441,7 +4162,7 @@ break; // otherwise ? } - afterDMO.setIgnoreBeforeTracking(true); + afterDMO.setIgnoreBeforeTracking(false); } } finally @@ -4454,6 +4175,10 @@ // if everything went fine, delete all data from this table deleteAll(); + + // and remove the origin-table link + ((AbstractTempTable) this.tableHandle().getResource()).setOriginTable(null); + ((AbstractTempTable) after.tableHandle().getResource()).setOriginTable(null); } catch (PersistenceException exc) { @@ -4646,9 +4371,9 @@ * @return {@code true} on success and {@code false} if any error is detected. */ protected static boolean copyParentUnchangedRecords(BufferImpl parentDst, - BufferImpl parentSrc, - BufferImpl childSrc, - String whereStr) + BufferImpl parentSrc, + BufferImpl childSrc, + String whereStr) { // What this method does: // for each [childSrc.before] @@ -4683,13 +4408,13 @@ while (!query._isOffEnd()) { - if (parentSrc.rowState().isUnknown() || - parentSrc.rowState().intValue() == Buffer.ROW_UNMODIFIED) + integer rowState = parentSrc.rowState(); + if (rowState.isUnknown() || rowState.intValue() == Buffer.ROW_UNMODIFIED) { parentDst.create(); Record dstDMO = parentDst.buffer().getCurrentRecord(); Record srcDMO = parentSrc.buffer().getCurrentRecord(); - copyDMO(dstDMO, srcDMO, parentDst, parentSrc); + BaseRecord.copy(dstDMO, srcDMO, false); parentDst.release(); } query.getNext(); @@ -4737,11 +4462,9 @@ switch (errorMode) { case COPY_TEMP_TABLE_MODE: - e = new NumberedException("COPY-TEMP-TABLE requires " + srcBufName + " and " + - dstBufName + " columns to match exactly unless the fourth loose-mode " + - "parameter is passed as TRUE", 12303, false); - ErrorManager.displayError(e.getNumber(), e.getMessage(), e.isPrefix()); - throw new InvalidParameterConditionException(e); + ErrorManager.recordOrShowError(12303, srcBufName, dstBufName); + // COPY-TEMP-TABLE requires and columns to match exactly unless the fourth loose-mode parameter is passed as TRUE. + break; case INPUT_PARAM_MODE: e = new NumberedException("The caller's temp-table parameter " + srcBufName + @@ -5051,6 +4774,12 @@ public void stateChanged(RecordChangeEvent event) throws PersistenceException { + Integer srcMPID = event.getSource().getMultiplexID(); + if (!srcMPID.equals(multiplexID)) + { + return; + } + if (event.isInsert()) { local.emptyTableFlags.put(multiplexID, Boolean.FALSE); @@ -5076,13 +4805,8 @@ // know our record must have been deleted. if (event.isFullBulkDelete()) { - Integer srcMPID = event.getSource().getMultiplexID(); - if (srcMPID.equals(multiplexID)) - { - release(false); - - return; - } + release(false); + return; } // Query whether the buffer's current record still exists. @@ -5376,6 +5100,68 @@ { return true; } + + /** + * Get the current primary key for the specified temp-table. + * + * @param dmoInterface + * Data model object interface. + * + * @return The current primary key. + */ + public long getCurrentPrimaryKey(Class dmoInterface) + { + return local.getCurrentPrimaryKey(dmoInterface); + } + + /** + * Set the primary key sequence for the specified temp-table to the new value. + * + * @param dmoIface + * Data model object interface. + * @param value + * The new value for the primary key sequence. + */ + public void setCurrentPrimaryKey(Class dmoIface, long value) + { + local.setCurrentPrimaryKey(dmoIface, value); + } + + /** + * Get this user's sequence name in the temporary database. + * + * @return User's sequence name in the temporary database. + */ + public String getSequenceName() + { + return local.sequenceName; + } + + /** + * Get the implicit SQL index name. + * + * @param dmoInterface + * DMO interface which represents the temp table. + * + * @return See the local method implementation. + */ + public String getImplicitSqlIndexName(Class dmoInterface) + { + return local.getImplicitSqlIndexName(dmoInterface); + } + + /** + * Get the implicit index name. + * + * @param dmoInterface + * DMO interface which represents the temp table. + * + * @return See the local method implementation. + */ + public String getImplicitIndexName(Class dmoInterface) + { + return local.getImplicitIndexName(dmoInterface); + } /** * Get the logical database name associated with this buffer. This will @@ -5569,7 +5355,7 @@ * @return Multiplex ID for this temporary buffer. */ @Override - protected Integer getMultiplexID() + public Integer getMultiplexID() { return multiplexID; } @@ -5789,16 +5575,19 @@ master.explicitBuffers.remove(mutableProxy); } - if (dynamicMultiplexer != null) + if (master == null || dynamicMultiplexer != null) { closeMultiplexScope(); - // remove multiplexer from TransactionManager's global finalizables, unless we are being - // invoked in the global scope, since it will be removed imminently anyway - int blockDepth = bufferManager.getCurrentScope(); - if (blockDepth > 0) + if (dynamicMultiplexer != null) { - txHelper.deregisterGlobalFinalizable(dynamicMultiplexer); + // remove multiplexer from TransactionManager's global finalizables, unless we are being + // invoked in the global scope, since it will be removed imminently anyway + int blockDepth = bufferManager.getCurrentScope(); + if (blockDepth > 0) + { + txHelper.deregisterGlobalFinalizable(dynamicMultiplexer); + } } } } @@ -5891,13 +5680,12 @@ * are defined as constants in {@link Buffer} interface. Additionally, {@code unknown} value is * returned if property is not available. * - * @return The current value of the {@code ROW-STATUS} property for this record as - * explained above. + * @return The current value of the {@code ROW-STATUS} property for this record as explained above. */ @Override protected integer rowState() { - if (!isAvailable()) + if (!isAvailable() || ((BufferImpl) getDMOProxy())._dataSet() == null) { return new integer(); } @@ -5935,7 +5723,8 @@ setupBeforeFlagImpl((r) -> r._rowState(state), Buffer.ROW_STATE_FIELD, - Buffer.__ROW_STATE__); + Buffer.__ROW_STATE__, + false); } /** @@ -5969,41 +5758,82 @@ { setupBeforeFlagImpl((r) -> r._peerRowid(peer.getValue()), Buffer.AFTER_ROWID_FIELD, - Buffer.__AFTER_ROWID__); + Buffer.__AFTER_ROWID__, + false); + } + + /** + * Sets the {@code origin-rowid} for AFTER-TABLE record. + * + * @param peer + * The new long rowid value of the record or {@code null}. + */ + @Override + public void originRowid(rowid peer) + { + setupBeforeFlagImpl((r) -> r._peerRowid(peer.getValue()), + Buffer.ORIGIN_ROWID_FIELD, + Buffer.__ORIGIN_ROWID__, + false); } /** * Gets {@code errorFlag} of this record. * - * @return the {@code errorFlag} of this record or {@code unknown} if it was not configured. + * @return the {@code errorFlag} of this record or {@code null} if it was not configured. If not null, + * the value is a bitwise combination of ERROR and REJECTED attribute. It's up to the called to + * extract the bit(s) it needs. * * @see Buffer#__ERROR_FLAG__ + * @see Buffer#ERROR_ERROR + * @see Buffer#ERROR_REJECTED */ @Override - protected integer errorFlag() + protected Integer errorFlags() { if (!isAvailable()) { - return new integer(); + return null; } - return new integer(getCurrentRecord()._errorFlag()); + return getCurrentRecord()._errorFlags(); } /** - * Sets the {@code errorFlag} value this peer record. + * Sets or removes the {@code errorFlag} value this peer record. The {@code error} attribute of the record + * is composed of multiple bit flags. This method manages its value based on the parameters it receives. * - * @param err - * The new error flag. + * @param errFlag + * The error bit to be set or removed. + * @param set + * Use {@code true} to set the flag and {@code false} to remove it. * * @see Buffer#__ERROR_FLAG__ + * @see Buffer#ERROR_ERROR + * @see Buffer#ERROR_REJECTED */ @Override - protected void errorFlag(integer err) + protected void updateErrorFlags(int errFlag, boolean set) { - setupBeforeFlagImpl((r) -> r._errorFlag(err.intValue()), - Buffer.ERROR_FLAG_FIELD, - Buffer.__ERROR_FLAG__); + setupBeforeFlagImpl( + r -> { + Integer errFlags = r._errorFlags(); + if (errFlags == null) + { + if (set) + { + r._errorFlags(errFlag); + } + // else let it unchanged (null) + } + else + { + r._errorFlags(set ? (errFlags | errFlag) : (errFlags & ~errFlag)); + } + }, + Buffer.ERROR_FLAG_FIELD, + Buffer.__ERROR_FLAG__, + true); } /** @@ -6037,7 +5867,8 @@ { setupBeforeFlagImpl((r) -> r._errorString(string.toJavaType()), Buffer.ERROR_STRING_FIELD, - Buffer.__ERROR_STRING__); + Buffer.__ERROR_STRING__, + true); } /** @@ -6220,7 +6051,7 @@ { String msg = "LIKE option " + like + " of the field " + getLegacyName() + "." + field.legacy + " must have a table qualifier"; - ErrorManager.displayError(msg, false); + ErrorManager.displayError(msg, false, null); return; } @@ -6231,7 +6062,7 @@ { String msg = "Could not find persistence for LIKE option " + like + " of the field " + getLegacyName() + "." + field.legacy; - ErrorManager.displayError(msg, false); + ErrorManager.displayError(msg, false, null); return; } @@ -6251,7 +6082,7 @@ { String msg = "Could not find like-field for LIKE option " + like + " of the field " + getLegacyName() + "." + field.legacy; - ErrorManager.displayError(msg, false); + ErrorManager.displayError(msg, false, null); return; } @@ -6536,7 +6367,7 @@ * @throws PersistenceException * if an error occurs performing the bulk delete operation. */ - protected void delete(String where, Object[] args) + protected void delete(String where, Object... args) throws PersistenceException { if (!hasOoDestructors()) @@ -6573,7 +6404,7 @@ * @throws PersistenceException * if an error occurs performing the bulk delete operation. */ - protected void delete(DataModelObject[] suppDMOs, String where, Object[] args) + protected void delete(DataModelObject[] suppDMOs, String where, Object... args) throws PersistenceException { if (!hasOoDestructors()) @@ -6604,6 +6435,13 @@ protected void delete(Supplier valexp, Supplier valmsg) throws PersistenceException { + if (createRowCallback) + { + // deleting a transient record, see [createRowCallback] javadoc + release(true, false); + return; + } + // check the BEFORE-BUFFER constraints BufferImpl buf = (BufferImpl) getDMOProxy(); if (buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints()) @@ -6623,7 +6461,7 @@ boolean autoCommit = !bufferManager.isTransaction(); Long id = null; - persistenceContext.beginTransaction(null); + autoCommit &= persistenceContext.beginTransaction(null); try { @@ -6636,10 +6474,10 @@ rowid b4Rowid = null; Long originRowid = null; BufferImpl after = (BufferImpl) getDMOProxy(); - if (!ignoreBeforeTracking && - buffer().isAvailable() && - after.isAfterBuffer() && - !after.dataSet().isUnknown() && + if (!ignoreBeforeTracking && + buffer().isAvailable() && + after.isAfterBuffer() && + after._dataSet() != null && TempTableBuilder.asTempTable((Temporary) after).isTrackingChanges().getValue()) { b4Copy = buffer().getCurrentRecord().snapshot(); @@ -6663,7 +6501,8 @@ before.findByRowID(b4Rowid); if (before._available()) { - if (before.rowState().intValue() == Buffer.ROW_CREATED) + integer rowState = before.rowState(); + if (!rowState.isUnknown() && rowState.intValue() == Buffer.ROW_CREATED) { before.deleteRecord(); } @@ -6672,14 +6511,14 @@ { // create new before record image, then update my before-rowid and row-state before.create(); - copyDMO(b4Buffer.getCurrentRecord(), b4Copy, before, after); + BaseRecord.copy(b4Buffer.getCurrentRecord(), b4Copy, false); } } else // ROW-STATE(before) == Buffer.ROW_UNMODIFIED { // create new before record image, then update my before-rowid and row-state before.create(); - copyDMO(b4Buffer.getCurrentRecord(), b4Copy, before, after); + BaseRecord.copy(b4Buffer.getCurrentRecord(), b4Copy, false); } if (b4Buffer.isAvailable()) @@ -6688,12 +6527,25 @@ b4Buffer.peerRowid(new rowid()); // keep the reference to the original datasource-rowid // TODO: is this only for DATA-SOURCE? because for a simple TEMP-TABLE (in a DATASET), this - // is not set. + // is not set. ((TempRecord) b4Buffer.getCurrentRecord())._originRowid(originRowid); } before.setUpBeforeBuffer(false); - // do not release the before buffer, let it hold the newly created row + // do not release the before buffer, let it hold the newly created row, but flush it! + + if (b4Buffer.isAvailable()) + { + try + { + b4Buffer.flush(); + } + catch (ValidationException e) + { + // this should not happen, there are no constraints to validate on before records + throw new PersistenceException(e); + } + } } } @@ -6716,7 +6568,7 @@ // TODO: what to do here? if (LOG.isLoggable(Level.WARNING)) { - LOG.log(Level.WARNING, "Error commiting delete", exc); + LOG.log(Level.WARNING, "Error committing delete", exc); } } } @@ -7237,29 +7089,43 @@ * The java name of the field to be updated. used to notify the {@code ChangeBroker}. * @param legacy * The legacy name of the field. Only used for logging when operation fails. + * @param updatePeer + * Update the peer too: make the same change to the peer record. */ - private void setupBeforeFlagImpl(BeforeFieldUpdater updater, String changedField, String legacy) + private void setupBeforeFlagImpl(BeforeFieldUpdater updater, + String changedField, + String legacy, + boolean updatePeer) { if (!isAvailable()) { return; } - + TempRecord record = getCurrentRecord(); TempRecord snapshot = (TempRecord) record.deepCopy(); - snapshot._rowState(record._rowState()); - snapshot._peerRowid(record._peerRowid()); - snapshot._errorFlag(record._errorFlag()); - snapshot._errorString(record._errorString()); - snapshot._originRowid(record._originRowid()); updater.update(record); + record.updateState(null, DmoState.CHANGED, true); try { Map> props = new HashMap<>(); props.put(changedField, null); - changeBroker.stateChanged( - RecordChangeEvent.Type.UPDATE, this, record, snapshot, props); + changeBroker.stateChanged(RecordChangeEvent.Type.UPDATE, this, record, snapshot, props); + + if (updatePeer && record._peerRowid() != null) + { + Class peerClass = + ((BufferImpl) getDMOProxy()).peerBuffer.buffer().getDMOImplementationClass(); + Session session = getPersistence().getSession(); + TempRecord peerRecord = (TempRecord) session.get(peerClass, record._peerRowid()); + TempRecord peerSnapshot = (TempRecord) peerRecord.deepCopy(); + updater.update(peerRecord); + peerRecord.updateState(null, DmoState.CHANGED, true); + + // reusing props + changeBroker.stateChanged(RecordChangeEvent.Type.UPDATE, this, peerRecord, peerSnapshot, props); + } } catch (PersistenceException pe) { @@ -8153,29 +8019,29 @@ private final Temporary def; /** A map between the definition-time DMO properties and their indexes. */ - private Map propertyIndexes = new HashMap<>(); - + private final Map propertyIndexes = new HashMap<>(); + /** The buffer instance created at the definition time. This is what {@link #def} proxies. */ - private TemporaryBuffer defBuffer; - + private final TemporaryBuffer defBuffer; + /** The DMO property setters to their property name. */ private Map defPropsBySetter; - + /** The DMO property getters to their property name. */ private Map defPropsByGetter; - + /** The bound DMO proxy. */ private Temporary bound = null; /** The bound DMO property names. */ private String[] boundProperties = null; - + /** The bound DMO property getters. */ private Map boundGetterByProp; - + /** The bound DMO property setters. */ private Map boundSetterByProp; - + /** The bound DMO buffer. This is what {@link #bound} proxies. */ private TemporaryBuffer boundBuffer; @@ -8248,7 +8114,6 @@ for (int i = 0; i < boundFields.size(); i++) { boundProperties[i] = boundFields.get(i).getJavaName(); - properties.put(cvtFields.get(i).getJavaName(), boundProperties[i]); } properties = Collections.unmodifiableMap(properties); === modified file 'src/com/goldencode/p2j/persist/annotation/Index.java' --- src/com/goldencode/p2j/persist/annotation/Index.java 2020-09-07 10:42:05 +0000 +++ src/com/goldencode/p2j/persist/annotation/Index.java 2020-12-25 12:48:37 +0000 @@ -2,13 +2,14 @@ ** Module : Index.java ** Abstract : Annotation used to store information about a legacy table index. ** -** Copyright (c) 2013-2019, Golden Code Development Corporation. +** Copyright (c) 2013-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 CA 20131021 Created initial version. ** 002 VMN 20131125 Added method primary(). ** 003 SVL 20160704 Added unique(), word(), components(). ** 004 ECF 20190831 Refactored from LegacyIndex. +** 005 IAS 20201224 Added 'wordtablename' attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -79,6 +80,9 @@ /** The name of the legacy table index. */ String legacy(); + + /** The names of the word table (for word index). */ + String wordtablename() default ""; /** Defines whether this table index is primary. */ boolean primary() default false; @@ -91,4 +95,4 @@ /** Array of index components (fields with sorting directions). */ IndexComponent[] components() default {}; -} \ No newline at end of file +} === modified file 'src/com/goldencode/p2j/persist/annotation/Property.java' --- src/com/goldencode/p2j/persist/annotation/Property.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/annotation/Property.java 2021-01-13 21:04:41 +0000 @@ -24,6 +24,8 @@ ** IAS 20200804 Added support for the field DESCRIPTION, CASE-SENSITIVE, DECIMALS, ** and MAX-WIDTH attributes ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** SVL 20201111 Use "reserved null" as a default value for LABEL and COLUMN-LABEL. +** OM 20201120 Fixed default value for scale/decimals. */ /* @@ -95,6 +97,12 @@ public @interface Property { /** + * "Reserved null". Used as a default value for nullable properties because you cannot set null as a + * default value for a property. Later it is converted to true null. + */ + String NULL_STRING = "__NULL_STRING__#!^+*__"; + + /** * The id of this field in the sequence of the fields of the buffer. It is needed by the * dynamic {@code bufferField(int)} attribute of the {@code BufferImpl} for * emulating the {@code BUFFER-FIELD(integer)} buffer handle attribute. @@ -144,13 +152,13 @@ boolean initialNull() default false; /** LABEL attribute of this field. */ - String label() default ""; + String label() default NULL_STRING; /** LABEL-SA option of this table. */ String label_sa() default ""; /** COLUMN-LABEL attribute of this field. */ - String columnLabel() default ""; + String columnLabel() default NULL_STRING; /** COLUMN-LABEL-SA attribute of this field. */ String columnLabel_sa() default ""; @@ -194,7 +202,7 @@ boolean caseSensitive() default false; /** Scale for decimal fields (decimals, precision, in 4GL terms). Other datatypes are not affected. */ - int scale() default 10; + int scale() default 0; /** Flag indicating the field should not be included in serialized output. */ boolean serializeHidden() default false; === modified file 'src/com/goldencode/p2j/persist/annotation/Table.java' --- src/com/goldencode/p2j/persist/annotation/Table.java 2020-09-07 10:42:05 +0000 +++ src/com/goldencode/p2j/persist/annotation/Table.java 2020-11-20 18:40:59 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2014-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 CA 20131015 Created initial version. ** 002 VMN 20140422 Added table() method. ** 003 SVL 20140502 Added hasValidation() method. @@ -15,6 +15,7 @@ ** 007 OM 20190520 Added AFTER-TABLE and BEFORE-TABLE temp-table names. ** 008 ECF 20190831 Refactored from LegacyTable. ** OM 20200108 Added "dirtyRead" attribute. +** 009 OM 20201120 Added "no-undo" attribute, specific to TEMP-TABLEs. */ /* @@ -105,6 +106,9 @@ /** CATEGORY option of this table. */ String category() default ""; + /** The NO-UNDO option, for TEMP-TABLES only. */ + boolean noUndo() default false; + /** If {@code true} then the table has delete validation. */ boolean hasValidation() default false; === modified file 'src/com/goldencode/p2j/persist/dialect/Dialect.java' --- src/com/goldencode/p2j/persist/dialect/Dialect.java 2020-09-23 23:47:02 +0000 +++ src/com/goldencode/p2j/persist/dialect/Dialect.java 2021-01-25 07:55:58 +0000 @@ -71,6 +71,8 @@ ** Dropped unused/obsolete methods. ** CA 20200624 Augment the index with the NULLS FIRST/LAST depending on the direction and dialect. ** 028 OM 20200924 Index components carry multiple information to avoid map lookups for them. +** 029 IAS 20201204 Added id() method and support for word tables +** IAS 20210105 Added isUdfContains() function */ /* @@ -128,6 +130,7 @@ package com.goldencode.p2j.persist.dialect; +import java.io.*; import java.sql.*; import java.sql.Date; import java.util.*; @@ -136,6 +139,7 @@ import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.orm.*; +import com.goldencode.p2j.persist.orm.DDLGeneratorWorker.*; import com.goldencode.p2j.persist.sequence.SequenceHandler; import com.goldencode.p2j.util.*; @@ -204,6 +208,16 @@ return dialect; } + /** + * Get known dialects by name + * + * @return known dialects by name + */ + public static Map dialects() + { + return Collections.unmodifiableMap(knownDialects); + } + /** * Get the instance of a concrete subclass of this class, as specified in the given settings. * If the settings do not specify a dialect class, {@code null} is returned. @@ -295,6 +309,48 @@ public abstract String getDriverClassName(); /** + * Get dialect id. + * @return dialect id + */ + public abstract String id(); + + /** + * Check if UDF should be used for converted CONTAINS operator + * + * @return true if UDF should be used for converted CONTAINS operator + */ + public abstract boolean useUdf4Contains(); + + /** + * Check if the provided name is the name of the CONTAINS UDF + * @param name + * name to be checked + * @return true if the provided name is the name of the CONTAINS UDF + */ + public boolean isUdfContains(String name) + { + return "contains".equals(name); + } + + /** + * Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all + * triggers in a database with a specific schema and using a certain dialect. + * + * @param dbName + * The target schema. + * @param out + * The {@code PrintStream} used for output. + * @param eoln + * OS-specific end of line terminator. + * @param wordTables + * word tables by name + */ + public abstract void generateWordTablesDDLImpl(String dbName, + PrintStream out, + String eoln, + Map wordTables); + + /** * Create a data object, the purpose of which is specific to a particular * dialect implementation, to be stored as a context-local object. This * allows a dialect to establish context-local data which is stored by the @@ -1105,8 +1161,10 @@ /** * Determine if the sort component or index needs to be augmented with a NULLS FIRST/LAST option. * - * @param asc - * The sort direction. + * @param asc + * The sort direction. + * + * @return NULLS FIRST/LAST string, if needed by this dialect. */ public String orderByNulls(boolean asc) { === modified file 'src/com/goldencode/p2j/persist/dialect/DialectHelper.java' --- src/com/goldencode/p2j/persist/dialect/DialectHelper.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/dialect/DialectHelper.java 2020-12-23 16:58:23 +0000 @@ -1,6 +1,6 @@ /* ** Module : DialectHelper.java -** Abstract : a helper to P2J dialect classes +** Abstract : A helper to P2J dialect classes ** ** Copyright (c) 2014-2020, Golden Code Development Corporation. ** @@ -9,6 +9,7 @@ ** 002 OM 20140410 Fixed support case-sensitive fields in indexes. ** 003 OM 20150525 Added static method getDialectClass(). ** 004 ECF 20200906 New ORM implementation. +** 005 IAS 20201219 Added support for "sqlserver2008" dialect. */ /* @@ -164,6 +165,8 @@ return P2JPostgreSQLDialect.class; case "sqlserver2012": return P2JSQLServer2012Dialect.class; + case "sqlserver2008": + return P2JSQLServer2008Dialect.class; default: return null; } === modified file 'src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java' --- src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java 2020-07-14 21:36:53 +0000 +++ src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java 2021-01-25 07:55:58 +0000 @@ -2,7 +2,7 @@ ** Module : P2JH2Dialect.java ** Abstract : Concrete implementation of Dialect for an H2 backend ** -** Copyright (c) 2008-2020, Golden Code Development Corporation. +** Copyright (c) 2008-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 ECF 20080223 @37519 Created initial version. Adds P2JDialect @@ -82,6 +82,9 @@ ** 031 OM 20200610 Replaced Hibernate with customized ORM. ** CA 20200624 Augment the sort with the NULLS FIRST/LAST depending on the direction and dialect. ** CA 20200714 Updated the reserved keywords with the 1.4.200 version. +** AIL 20201120 Changed syntax for sequence nextval. +** IAS 20201204 Added id() method and support stub for word tables +** IAS 20210104 Added trigger's for the population/update word tables */ /* @@ -139,6 +142,7 @@ package com.goldencode.p2j.persist.dialect; +import java.io.*; import java.net.InetAddress; import java.net.URI; import java.sql.*; @@ -147,7 +151,11 @@ import java.util.List; import java.util.Map; import java.util.logging.*; +import java.util.stream.*; +import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.persist.*; +import com.goldencode.p2j.persist.h2.*; +import com.goldencode.p2j.persist.orm.DDLGeneratorWorker.*; import com.goldencode.p2j.persist.sequence.*; import com.goldencode.p2j.util.*; @@ -163,6 +171,9 @@ /** Logger */ private static final Logger LOG = LogHelper.getLogger(P2JH2Dialect.class.getName()); + /** Name of the primary key column. */ + private static final String PK = Configuration.getSchemaConfig().getPrimaryKeyName(); + /** Mapping between the FWD possible field/column types and their SQL counterparts. */ private static Map fwd2sql = new HashMap<>(); @@ -190,6 +201,31 @@ fwd2sql.put("raw", "binary"); } + /** AFTER INSERT trigger for words tables' support template */ + private static final List AFTER_INSERT_TRIGGER = Collections.unmodifiableList( + Arrays.asList( + "CREATE TRIGGER %s AFTER INSERT ON %s FOR EACH ROW AS", + "$$org.h2.api.Trigger create() { return new %s(\"%s;%s;%s;%s\"); } $$;" + ) + ); + + /** AFTER UPDATE trigger for words tables' support template */ + private static final List AFTER_UPDATE_TRIGGER = Collections.unmodifiableList( + Arrays.asList( + "CREATE TRIGGER %s AFTER UPDATE ON %s FOR EACH ROW AS", + "$$org.h2.api.Trigger create() { return new %s(\"%s;%s;%s;%s\"); } $$;" + ) + ); + + /* +CREATE TRIGGER IF2_UPD AFTER UPDATE ON TTWIABCDEFGHIJKLMNOPQRSTUVWXYXZ FOR EACH ROW AS +$$org.h2.api.Trigger create() { return new com.goldencode.p2j.persist.h2.OnUpdateWords("RECID;IF2ABCDEFGHIJKLMNOPQRSTUVWXYXZ;TTWIABCDEFGHIJKLMNOPQRSTUVWXYXZ__IF2ABCDEFGHIJKLMNOPQRSTUVWXYXZ;true"); } $$; + +CREATE TRIGGER IF1_INS AFTER INSERT ON TTWIABCDEFGHIJKLMNOPQRSTUVWXYXZ FOR EACH ROW AS +$$org.h2.api.Trigger create() { return new com.goldencode.p2j.persist.h2.OnInsertWords("RECID;IF1ABCDEFGHIJKLMNOPQRSTUVWXYXZ;TTWIABCDEFGHIJKLMNOPQRSTUVWXYXZ__IF1ABCDEFGHIJKLMNOPQRSTUVWXYXZ;false"); } $$; + + */ + // /** Actual minimum interval in milliseconds between analyze commands */ // private static final long analyzeInterval = 60000; @@ -740,7 +776,7 @@ @Override public String getSequencePrefetchString(String sequenceName) { - return "select nextval('" + sequenceName + "') from system_range(?, ?)"; + return "select " + sequenceName + ".nextval from system_range(?, ?)"; } /** @@ -1356,4 +1392,74 @@ throw new RuntimeException(e); } } + + /** + * Check if UDF should be used for converted CONTAINS operator + * + * @return true if UDF should be used for converted CONTAINS operator. + */ + @Override + public boolean useUdf4Contains() + { + return false; + } + + /** + * Check if the provided name is the name of the CONTAINS UDF + * @param name + * name to be checked + * @return true if the provided name is the name of the CONTAINS UDF + */ + public boolean isUdfContains(String name) + { + return "contains_1".equals(name) || "contains_2".equals(name); + } + + /** + * Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all + * triggers in a database with a specific schema and using a certain dialect. + * + * @param dbName + * The target schema. + * @param out + * The {@code PrintStream} used for output. + * @param eoln + * OS-specific end of line terminator. + * @param wordTables + * word tables by name + */ + @Override + public void generateWordTablesDDLImpl(String dbName, + PrintStream out, + String eoln, + Map wordTables) + { + String afterInsertTrigger = AFTER_INSERT_TRIGGER.stream().collect(Collectors.joining(eoln)); + String afterUpdateTrigger = AFTER_UPDATE_TRIGGER.stream().collect(Collectors.joining(eoln)); + wordTables.values().stream().forEach(wt -> { + out.printf(afterInsertTrigger, + wt.afterInsertTriggerName, wt.parentTableName, + (wt.extent == 0 ? OnInsertWords.class : OnInsertWordsExt.class).getName(), + PK, wt.fieldName, wt.tableName, wt.caseSensitive); + out.print(eoln); + out.print(eoln); + out.printf(afterUpdateTrigger, + wt.afterUpdateTriggerName, wt.parentTableName, + (wt.extent == 0 ? OnUpdateWords.class : OnUpdateWordsExt.class).getName(), + PK, wt.fieldName, wt.tableName, wt.caseSensitive); + out.print(eoln); + out.print(eoln); + }); + } + + /** + * Get dialect id. + * + * @return Dialect id. + */ + @Override + public String id() + { + return "h2"; + } } === modified file 'src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java' --- src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java 2020-07-16 18:02:16 +0000 +++ src/com/goldencode/p2j/persist/dialect/P2JPostgreSQLDialect.java 2021-01-29 19:50:19 +0000 @@ -101,6 +101,7 @@ ** 038 ECF 20190327 Replaced ContextLocal for inlineLimit variable with more efficient ** ThreadLocal. It is always accessed on the same thread. ** 039 OM 20200610 Removed Hibernate dependecies. +** 040 IAS 20201204 Added id() method and support for word tables */ /* @@ -164,14 +165,17 @@ import java.text.*; import java.util.*; import java.util.logging.*; +import java.util.stream.*; import org.apache.commons.io.*; import org.apache.commons.io.input.*; import com.goldencode.p2j.util.LogHelper; import com.goldencode.util.*; +import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.directory.DirectoryService; import com.goldencode.p2j.persist.*; +import com.goldencode.p2j.persist.orm.DDLGeneratorWorker.*; import com.goldencode.p2j.persist.sequence.*; import com.goldencode.p2j.util.*; @@ -189,6 +193,109 @@ /** Logger */ private static final Logger LOG = LogHelper.getLogger(P2JPostgreSQLDialect.class.getName()); + /** Name of the primary key column. */ + private static final String PK = Configuration.getSchemaConfig().getPrimaryKeyName(); + + /** words function template */ + private static final List WORDS_FUNC = Collections.unmodifiableList( + Arrays.asList( + "create or replace function words (", + " recid int8, txt text, toUpperCase boolean", + " )", + " returns table ( parent int8, word text ) language 'plpgsql' as", + " $$", + " declare arr text[];", + " declare w text;", + " begin", + // NB: uppercase at the database side + " arr = words(txt, false);", + " foreach w in array arr loop", + " parent = recid;", + " word = case when touppercase then UPPER(w) else w end;", + " return next;", + " end loop;", + " end", + " $$;" + ) + ); + + /** words function template (extent field*/ + private static final List WORDS_FUNC_EXT = Collections.unmodifiableList( + Arrays.asList( + "create or replace function words (", + " recid int8, listidx int4, txt text, toUpperCase boolean", + " )", + " returns table ( parent int8, idx int4, word text ) language 'plpgsql' as", + " $$", + " declare arr text[];", + " declare w text;", + " begin", + // NB: uppercase at the database side + " arr = words(txt, false);", + " foreach w in array arr loop", + " parent = recid;", + " idx = listidx;", + " word = case when touppercase then UPPER(w) else w end;", + " return next;", + " end loop;", + " end", + " $$;" + ) + ); + + /** trigger function for words tables' support template */ + private static final List TRIGGER_FUNC = Collections.unmodifiableList( + Arrays.asList( + "create or replace function %s()", + "returns trigger", + "language plpgsql", + "as", + "$$", + "begin", + " delete from %s where %s.parent__id = new.%s;", + " insert into %s select * from words(new.%s, new.%s, %s);", + " return new;", + "end;", + "$$;" + ) + ); + + /** trigger function for words tables' support template (extent field*/ + private static final List TRIGGER_FUNC_EXT = Collections.unmodifiableList( + Arrays.asList( + "create or replace function %s()", + "returns trigger", + "language plpgsql", + "as", + "$$", + "begin", + " delete from %s where %s.parent__id = new.%s and %s.list__index = new.list__index;", + " insert into %s select * from words(new.%s, new.list__index, new.%s, %s);", + " return new;", + "end;", + "$$;" + ) + ); + /** AFTER INSERT trigger for words tables' support template */ + private static final List AFTER_INSERT_TRIGGER = Collections.unmodifiableList( + Arrays.asList( + "create trigger %s after", + "insert", + " on", + " %s for each row execute procedure %s();" + ) + ); + + /** AFTER UPDATE trigger for words tables' support template */ + private static final List AFTER_UPDATE_TRIGGER = Collections.unmodifiableList( + Arrays.asList( + "create trigger %s after", + "update of %s", + " on", + " %s for each row execute procedure %s();" + ) + ); + /** Default minimum interval in seconds between temp table vacuums */ private static final int DEFAULT_VACUUM_INTERVAL = 30; @@ -1899,4 +2006,81 @@ throw new SQLException("FWDClob:truncate"); } } + + /** + * Check if UDF should be used for converted CONTAINS operator + * + * @return true if UDF should be used for converted CONTAINS operator + */ + @Override + public boolean useUdf4Contains() + { + return false; + } + + /** + * Get dialect id. + * + * @return Dialect id + */ + @Override + public String id() + { + return "postgresql"; + } + + /** + * Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all + * triggers in a database with a specific schema and using a certain dialect. + * + * @param dbName + * The target schema. + * @param out + * The {@code PrintStream} used for output. + * @param eoln + * OS-specific end of line terminator. + * @param wordTables + * word tables by name + */ + @Override + public void generateWordTablesDDLImpl(String dbName, + PrintStream out, + String eoln, + Map wordTables) + { + // Create 'words' function + WORDS_FUNC.stream().forEach(l -> {out.print(l); out.print(eoln);}); + out.print(eoln); + WORDS_FUNC_EXT.stream().forEach(l -> {out.print(l); out.print(eoln);}); + out.print(eoln); + String triggerFunc = TRIGGER_FUNC.stream().collect(Collectors.joining(eoln)); + String triggerFuncExt = TRIGGER_FUNC_EXT.stream().collect(Collectors.joining(eoln)); + String afterInsertTrigger = AFTER_INSERT_TRIGGER.stream().collect(Collectors.joining(eoln)); + String afterUpdateTrigger = AFTER_UPDATE_TRIGGER.stream().collect(Collectors.joining(eoln)); + wordTables.values().stream().forEach(wt -> { + if (wt.extent == 0) + { + out.printf(triggerFunc, + wt.triggerFunctionName, wt.tableName, wt.tableName, PK, + wt.tableName, PK, wt.fieldName, !wt.caseSensitive); + } + else + { + out.printf(triggerFuncExt, + wt.triggerFunctionName, wt.tableName, wt.tableName, PK, + wt.tableName, PK, wt.tableName, wt.fieldName, !wt.caseSensitive); + } + out.print(eoln); + out.print(eoln); + out.printf(afterInsertTrigger, + wt.afterInsertTriggerName, wt.parentTableName, wt.triggerFunctionName); + out.print(eoln); + out.print(eoln); + out.printf(afterUpdateTrigger, + wt.afterUpdateTriggerName, wt.fieldName, wt.parentTableName, wt.triggerFunctionName); + out.print(eoln); + out.print(eoln); + }); + } + } === modified file 'src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java' --- src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java 2020-09-27 18:16:32 +0000 +++ src/com/goldencode/p2j/persist/dialect/P2JSQLServer2008Dialect.java 2021-01-25 07:55:58 +0000 @@ -30,6 +30,7 @@ ** 013 OM 20200610 Removed Hibernate dependencies. ** 014 OM 20200924 Index components carry multiple information to avoid map lookups for them. ** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. +** 015 IAS 20201204 Added id() method and support stub for word tables */ /* @@ -87,6 +88,7 @@ package com.goldencode.p2j.persist.dialect; +import java.io.*; import java.sql.*; import java.sql.Date; import java.text.NumberFormat; @@ -95,6 +97,7 @@ import java.util.Map; import java.util.logging.*; import com.goldencode.p2j.persist.orm.*; +import com.goldencode.p2j.persist.orm.DDLGeneratorWorker.*; import com.goldencode.p2j.util.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.sequence.SequenceHandler; @@ -1260,4 +1263,48 @@ throw new RuntimeException(e); } } + + /** + * Check if UDF should be used for converted CONTAINS operator + * + * @return true if UDF should be used for converted CONTAINS operator + */ + @Override + public boolean useUdf4Contains() + { + return true; + } + + /** + * Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all + * triggers in a database with a specific schema and using a certain dialect. + * + * @param dbName + * The target schema. + * @param out + * The {@code PrintStream} used for output. + * @param eoln + * OS-specific end of line terminator. + * @param wordTables + * word tables by name + */ + @Override + public void generateWordTablesDDLImpl(String dbName, + PrintStream out, + String eoln, + Map wordTables) + { + // TODO implement + } + + /** + * Get dialect id. + * + * @return Dialect id + */ + @Override + public String id() + { + return "sqlserver2008"; + } } === modified file 'src/com/goldencode/p2j/persist/dialect/P2JSQLServer2012Dialect.java' --- src/com/goldencode/p2j/persist/dialect/P2JSQLServer2012Dialect.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/dialect/P2JSQLServer2012Dialect.java 2021-01-25 07:55:58 +0000 @@ -16,6 +16,7 @@ ** 007 ECF 20160912 Added no-op getSequencePrefetchString as a placeholder. Needs an ** implementation which can generate a series like PostgreSQL dialect. ** 008 ECF 20200906 New ORM implementation. +** 008 IAS 20201204 Added id() method and support stub for word tables */ /* @@ -73,7 +74,9 @@ package com.goldencode.p2j.persist.dialect; +import com.goldencode.p2j.persist.orm.DDLGeneratorWorker.*; import com.goldencode.p2j.persist.sequence.*; +import java.io.*; import java.util.*; /** @@ -535,4 +538,37 @@ { buf.insert(selectStartPos + "select".length(), " top " + limit); } + + /** + * Get dialect id. + * + * @return Dialect id + */ + @Override + public String id() + { + return "sqlserver2012"; + } + + /** + * Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all + * triggers in a database with a specific schema and using a certain dialect. + * + * @param dbName + * The target schema. + * @param out + * The {@code PrintStream} used for output. + * @param eoln + * OS-specific end of line terminator. + * @param wordTables + * word tables by name + */ + @Override + public void generateWordTablesDDLImpl(String dbName, + PrintStream out, + String eoln, + Map wordTables) + { + // TODO implement + } } === modified file 'src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java' --- src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java 2020-09-29 22:47:24 +0000 +++ src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java 2020-10-14 21:06:19 +0000 @@ -74,6 +74,7 @@ ** 027 ECF 20160608 Removed unnecessary intern calls. ** 028 ECF 20200906 New ORM implementation. ** 029 EVL 20200621 Adding NPE protection for lockSingle when index name is NULL or empty. +** 030 OM 20201012 Force use locally cached meta information instead of map lookup. */ /* @@ -1715,8 +1716,8 @@ return; } - Class dmoClass = DBUtils.dmoClassForEntity(entity); - Class dmoIface = DmoMetadataManager.getDMOInterface(dmoClass); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null); + Class dmoIface = dmoInfo.getAnnotatedInterface(); Session session = null; Connection conn = null; Statement stmt = null; === added directory 'src/com/goldencode/p2j/persist/h2' === added file 'src/com/goldencode/p2j/persist/h2/OnInsertWords.java' --- src/com/goldencode/p2j/persist/h2/OnInsertWords.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/h2/OnInsertWords.java 2021-01-05 13:24:19 +0000 @@ -0,0 +1,115 @@ +/* +** Module : OnInsertWords.java +** Abstract : AFTER INSERT trigger populating word table +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 IAS 20210104 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.h2; + +import java.sql.*; + +/** + * AFTER INSERT trigger populating word table + */ +public class OnInsertWords +extends WordsTrigger +{ + + /** + * Constructor + * @param init + * initialization string. Contains the following values + * separated by semicolon: + * - the name of the primary key of the master table + * - the name of the field of the master table + * - the word table name + * - the flag indicating that the field is case sensitive ('true'/'false') + */ + public OnInsertWords(String init) + { + super(init); + } + + /** + * This method is called for each triggered action by the default + * fire(Connection conn, Object[] oldRow, Object[] newRow) method. + * ResultSet.next does not need to be called (and calling it has no effect; + * it will always return true). + *

+ * For "before" triggers, the new values of the new row may be changed + * using the ResultSet.updateX methods. + *

+ * + * @param conn a connection to the database + * @param oldRow the old row, or null if no old row is available (for + * INSERT) + * @param newRow the new row, or null if no new row is available (for + * DELETE) + * @throws SQLException if the operation must be undone + */ + @Override + public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) + throws SQLException + { + long pk = newRow.getLong(pkName); + String field = newRow.getString(fieldName); + insertWords(conn, pk, field); + } +} === added file 'src/com/goldencode/p2j/persist/h2/OnInsertWordsExt.java' --- src/com/goldencode/p2j/persist/h2/OnInsertWordsExt.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/h2/OnInsertWordsExt.java 2021-01-16 11:18:16 +0000 @@ -0,0 +1,116 @@ +/* +** Module : OnInsertWordsExt.java +** Abstract : AFTER INSERT trigger populating word table (extent fields) +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 IAS 20210116 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.h2; + +import java.sql.*; + +/** + * AFTER INSERT trigger populating word table (extent fields) + */ +public class OnInsertWordsExt +extends WordsTrigger +{ + + /** + * Constructor + * @param init + * initialization string. Contains the following values + * separated by semicolon: + * - the name of the primary key of the master table + * - the name of the field of the master table + * - the word table name + * - the flag indicating that the field is case sensitive ('true'/'false') + */ + public OnInsertWordsExt(String init) + { + super(init); + } + + /** + * This method is called for each triggered action by the default + * fire(Connection conn, Object[] oldRow, Object[] newRow) method. + * ResultSet.next does not need to be called (and calling it has no effect; + * it will always return true). + *

+ * For "before" triggers, the new values of the new row may be changed + * using the ResultSet.updateX methods. + *

+ * + * @param conn a connection to the database + * @param oldRow the old row, or null if no old row is available (for + * INSERT) + * @param newRow the new row, or null if no new row is available (for + * DELETE) + * @throws SQLException if the operation must be undone + */ + @Override + public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) + throws SQLException + { + long pk = newRow.getLong(pkName); + int index = newRow.getInt(IDX); + String field = newRow.getString(fieldName); + insertWords(conn, pk, index, field); + } +} === added file 'src/com/goldencode/p2j/persist/h2/OnUpdateWords.java' --- src/com/goldencode/p2j/persist/h2/OnUpdateWords.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/h2/OnUpdateWords.java 2021-01-05 13:24:19 +0000 @@ -0,0 +1,129 @@ +/* +** Module : OnUpdateWords.java +** Abstract : AFTER UPDATE trigger populating word table +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 IAS 20210104 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.h2; + +import java.sql.*; + +/** + * AFTER UPDATE trigger populating word table + */ +public class OnUpdateWords +extends WordsTrigger +{ + + /** + * Constructor + * @param init + * initialization string. Contains the following values + * separated by semicolon: + * - the name of the primary key of the master table + * - the name of the field of the master table + * - the word table name + * - the flag indicating that the field is case sensitive ('true'/'false') + */ + public OnUpdateWords(String init) + { + super(init); + } + + /** + * This method is called for each triggered action by the default + * fire(Connection conn, Object[] oldRow, Object[] newRow) method. + * ResultSet.next does not need to be called (and calling it has no effect; + * it will always return true). + *

+ * For "before" triggers, the new values of the new row may be changed + * using the ResultSet.updateX methods. + *

+ * + * @param conn a connection to the database + * @param oldRow the old row, or null if no old row is available (for + * INSERT) + * @param newRow the new row, or null if no new row is available (for + * DELETE) + * @throws SQLException if the operation must be undone + */ + @Override + public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) + throws SQLException + { + long pk = newRow.getLong(pkName); + String field = newRow.getString(fieldName); + long oldPk = oldRow.getLong(pkName); + if ( oldPk != pk) + { + throw new SQLException("PK mismatch: " + pk + "!=" + oldPk); + } + try(PreparedStatement pstmt = conn.prepareStatement( + "DELETE FROM " + wordTableName + " WHERE PARENT__ID = ?" + )) + { + pstmt.setLong(1, pk); + int result = pstmt.executeUpdate(); + } + + insertWords(conn, pk, field); + } + +} === added file 'src/com/goldencode/p2j/persist/h2/OnUpdateWordsExt.java' --- src/com/goldencode/p2j/persist/h2/OnUpdateWordsExt.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/h2/OnUpdateWordsExt.java 2021-01-25 07:55:58 +0000 @@ -0,0 +1,133 @@ +/* +** Module : OnUpdateWordsExt.java +** Abstract : AFTER UPDATE trigger populating word table (extent fields) +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 IAS 20210116 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.h2; + +import java.sql.*; + +/** + * AFTER UPDATE trigger populating word table (extent fields) + */ +public class OnUpdateWordsExt +extends WordsTrigger +{ + + /** + * Constructor + * @param init + * initialization string. Contains the following values + * separated by semicolon: + * - the name of the primary key of the master table + * - the name of the field of the master table + * - the word table name + * - the flag indicating that the field is case sensitive ('true'/'false') + */ + public OnUpdateWordsExt(String init) + { + super(init); + } + + /** + * This method is called for each triggered action by the default + * fire(Connection conn, Object[] oldRow, Object[] newRow) method. + * ResultSet.next does not need to be called (and calling it has no effect; + * it will always return true). + *

+ * For "before" triggers, the new values of the new row may be changed + * using the ResultSet.updateX methods. + *

+ * + * @param conn a connection to the database + * @param oldRow the old row, or null if no old row is available (for + * INSERT) + * @param newRow the new row, or null if no new row is available (for + * DELETE) + * @throws SQLException if the operation must be undone + */ + @Override + public void fire(Connection conn, ResultSet oldRow, ResultSet newRow) + throws SQLException + { + long pk = newRow.getLong(pkName); + int index = newRow.getInt(IDX); + String field = newRow.getString(fieldName); + long oldPk = oldRow.getLong(pkName); + int oldIndex = oldRow.getInt(IDX); + if ( oldPk != pk || oldIndex != index) + { + throw new SQLException("PK mismatch: (" + + pk + "," + index + ") != (" + oldPk + "," + oldIndex + ")"); + } + try(PreparedStatement pstmt = conn.prepareStatement( + "DELETE FROM " + wordTableName + " WHERE PARENT__ID = ? AND LIST__INDEX = ?" + )) + { + pstmt.setLong(1, pk); + pstmt.setInt(2, index); + int result = pstmt.executeUpdate(); + } + + insertWords(conn, pk, index, field); + } + +} === added file 'src/com/goldencode/p2j/persist/h2/WordsTrigger.java' --- src/com/goldencode/p2j/persist/h2/WordsTrigger.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/h2/WordsTrigger.java 2021-01-29 19:50:19 +0000 @@ -0,0 +1,188 @@ +/* +** Module : WordsTrigger.java +** Abstract : Base class for triggers populating word table +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 IAS 20210104 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.h2; + +import java.sql.*; + +import org.h2.tools.*; + +import com.goldencode.p2j.persist.pl.*; + +/** + * Base class for triggers populating word table + */ +public abstract class WordsTrigger +extends TriggerAdapter +{ + /** Index field name in the extent table */ + protected static final String IDX = "list__index"; + /** the name of the primary key of the master table */ + protected final String pkName; + /** the name of the field of the master table */ + protected final String fieldName; + /** the word table name */ + protected final String wordTableName; + /** the flag indicating that the field is case sensitive */ + protected final boolean caseSensitive; + + /** + * Constructor + * @param init + * initialization string. Contains the following values + * separated by semicolon: + * - the name of the primary key of the master table + * - the name of the field of the master table + * - the word table name + * - the flag indicating that the field is case sensitive ('true'/'false') + */ + public WordsTrigger(String init) + { + super(); + String[] params = init.split(";"); + this.pkName = params[0]; + this.fieldName = params[1]; + this.wordTableName = params[2]; + this.caseSensitive = Boolean.parseBoolean(params[3]); + } + + /** + * Populate the word table associated with a field of the master table + * @param conn + * a connection to the database. + * @param pk + * the value of the primary key of the master table record + * @param field + * the value of the field the word table is associated with + * @throws SQLException + * if the operation must be undone + */ + protected void insertWords(Connection conn, long pk, String field) + throws SQLException + { + // NB: uppercase at the database side + String[] words = Functions.words(field, false); + if (words == null || words.length == 0) + { + return; + } + StringBuilder sql = new StringBuilder("INSERT INTO "). + append(wordTableName).append(" VALUES"); + for (int i = 0; i < words.length; i++) + { + // NB: uppercase at the database side + sql.append(i == 0 ? "" : ",").append(caseSensitive ? "(?,?)" : "(?,UPPER(?))"); + } + try(PreparedStatement pstmt = conn.prepareStatement(sql.toString())) + { + int i = 1; + for(String word: words) + { + pstmt.setLong(i++, pk); + pstmt.setString(i++, word); + } + pstmt.executeUpdate(); + }; + } + /** + * Populate the word table associated with an extent field of the master table + * @param conn + * a connection to the database. + * @param pk + * the value of the primary key of the master table record + * @param index + * the value of the index key of the extent table record + * @param field + * the value of the field the word table is associated with + * @throws SQLException + * if the operation must be undone + */ + protected void insertWords(Connection conn, long pk, int index, String field) + throws SQLException + { + // NB: uppercase at the database side + String[] words = Functions.words(field, false); + if (words == null || words.length == 0) + { + return; + } + StringBuilder sql = new StringBuilder("INSERT INTO "). + append(wordTableName).append(" VALUES"); + for (int i = 0; i < words.length; i++) + { + // NB: uppercase at the database side + sql.append(i == 0 ? "" : ",").append(caseSensitive ? "(?,?,?)" : "(?,?,UPPER(?))"); + } + try(PreparedStatement pstmt = conn.prepareStatement(sql.toString())) + { + int i = 1; + for(String word: words) + { + pstmt.setLong(i++, pk); + pstmt.setInt(i++, index); + pstmt.setString(i++, word); + } + pstmt.executeUpdate(); + }; + } +} === modified file 'src/com/goldencode/p2j/persist/hql/DataTypeHelper.java' --- src/com/goldencode/p2j/persist/hql/DataTypeHelper.java 2020-09-27 18:16:32 +0000 +++ src/com/goldencode/p2j/persist/hql/DataTypeHelper.java 2020-11-27 16:16:21 +0000 @@ -34,6 +34,7 @@ ** 020 CA 20200610 Initialize this class, as can be used outside of the FWD environment, for ** appserver clients. ** 021 CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. +** 022 IAS 20201123 Added ARRAY type */ /* @@ -500,6 +501,7 @@ classes.put(Date.class , FqlType.DATE); classes.put(Timestamp.class , FqlType.DATETIME); classes.put(TimestampWithTimeZone.class, FqlType.DATETIMETZ); + classes.put(String[].class , FqlType.ARRAY); //classes.put(Object[].class , HQLTypes.DATETIMETZ); // datetimetz is represented by a 2-uple (Timestamp, Offset) // P2J wrapper types. @@ -518,7 +520,7 @@ classes.put(comhandle.class , FqlType.TEXT); classes.put(object.class , FqlType.TEXT); classes.put(ObjectVar.class , FqlType.TEXT); - + bdts.put(Types.BOOLEAN, logical.class); bdts.put(Types.INTEGER, integer.class); // bdts.put(Types.?, recid.class); === modified file 'src/com/goldencode/p2j/persist/meta/LockTableUpdater.java' --- src/com/goldencode/p2j/persist/meta/LockTableUpdater.java 2020-09-24 17:51:18 +0000 +++ src/com/goldencode/p2j/persist/meta/LockTableUpdater.java 2020-10-14 21:06:19 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2013-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20131101 Created initial version. ** 002 ECF 20140116 Use session ID as user number (id) for lock events. ** 003 ECF 20140213 Change query strings to use JPA-style query substitution placeholders. @@ -22,6 +22,7 @@ ** 013 OM 20190111 Added support for LockChain field. ** 014 ECF 20200906 New ORM implementation. ** 015 CA 20200924 Replaced Method.invoke with ReflectASM. +** OM 20201012 Force use locally cached meta information instead of map lookup. */ /* @@ -277,8 +278,8 @@ String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaLock"; try { - implementingClass = DmoMetadataManager.getImplementingClass( - (Class) Class.forName(dmoName)); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(Class.forName(dmoName)); + implementingClass = dmoInfo.getImplementationClass(); // map MinimalLock methods to corresponding dmoClass methods; if we try to use // MinimalLock methods with a MetaLockImpl object, we will raise @@ -286,8 +287,7 @@ // instance of MinimalLock for (Method ifcMeth : MinimalLock.class.getMethods()) { - Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(), - ifcMeth.getParameterTypes()); + Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(), ifcMeth.getParameterTypes()); methodMap.put(ifcMeth, dmoMeth); } } === modified file 'src/com/goldencode/p2j/persist/meta/MetadataManager.java' --- src/com/goldencode/p2j/persist/meta/MetadataManager.java 2020-10-03 16:02:03 +0000 +++ src/com/goldencode/p2j/persist/meta/MetadataManager.java 2021-01-17 15:39:44 +0000 @@ -31,6 +31,8 @@ ** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** CA 20201003 Use an identity HashSet where possible. +** OM 20201012 Force use locally cached meta information instead of map lookup. +** IAS 20201210 Added support for word tables. */ /* @@ -109,6 +111,7 @@ import com.goldencode.p2j.persist.annotation.Trigger; import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.orm.*; +import com.goldencode.p2j.persist.orm.DmoMeta.*; import com.goldencode.p2j.util.*; import com.goldencode.p2j.util.LogHelper; import com.goldencode.util.*; @@ -170,6 +173,12 @@ /** Logger */ private static final Logger log = LogHelper.getLogger(MetadataManager.class); + /** Do we log WARNING-level events ? */ + private static final boolean LOG_WARNING = log.isLoggable(Level.WARNING); + + /** Do we log INFO-level events ? */ + private static final boolean LOG_INFO = log.isLoggable(Level.INFO); + /** The type of PK field. It is the only one that does not extend {@link BaseDataType}. */ private static final String PK_TYPE = "j"; @@ -286,6 +295,10 @@ private static final ConcurrentMap, integer>> FILE_NUM = new ConcurrentHashMap<>(); + /** Word tables' names for the primary databases */ + private static final ConcurrentMap>> WORD_TABLES = + new ConcurrentHashMap<>(); + /** * Private constructor; this class is not instantiated. */ @@ -347,7 +360,7 @@ if (!active) { - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Metadata server was not started; no metadata in use"); } @@ -396,7 +409,7 @@ catch (Exception e) { // catch all error cases; just skip the table, but let a message in log - if (log.isLoggable(Level.WARNING)) + if (LOG_WARNING) { log.log(Level.WARNING, "Failed to load _meta DMO: " + dmoIfaceName, e); } @@ -407,7 +420,7 @@ dmoMetaTypes.add(dmoIface); } - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Metadata server modules: " + helpers.keySet().toString()); } @@ -427,14 +440,14 @@ DatabaseManager.MIXED_MODE_SSL_PORT, false); String portString = String.valueOf(port); - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Starting secure, mixed mode, H2 metaschema server on port " + portString + "..."); } H2Helper.startMixedModeServer("-tcpSSL", "-tcpPort", portString, "-tcpAllowOthers"); - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Successfully started secure mixed mode H2 server"); } @@ -587,7 +600,7 @@ { if (!Configuration.getSchemaConfig().isMetadataActive()) { - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Metadata server was not started; no metadata in use"); } @@ -635,8 +648,8 @@ { Element record = (Element) recNodes.item(i); Record dmo = prepareRecord(record, schema); - Class iface = DmoMetadataManager.getDMOInterface(dmo.getClass()); - SystemTable systemTable = SystemTable.forIface(iface.getSimpleName()); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmo.getClass()); + SystemTable systemTable = SystemTable.forIface(dmoInfo.getAnnotatedInterface().getSimpleName()); if (systemTable != null && systemTable.isIgnoredInMetaXML()) { continue; @@ -659,7 +672,7 @@ throw new PersistenceException("Error populating metadata database " + metaDb, exc); } - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "Metaschema database " + metaDb + " ready"); log.log(Level.INFO, "Caching " + templateRecords.size() + " template record ids"); @@ -667,6 +680,33 @@ } /** + * Get the word index data. + * + * @param dbName + * Database name. + * @param tableName + * Master table name. + * @param fieldName + * Field name. + * + * @return The word index data. + */ + public static WordIndexData getWordTableName(String dbName, String tableName, String fieldName) + { + Map> tables = WORD_TABLES.get(dbName); + if (tables == null) + { + return null; + } + Map fields = tables.get(tableName); + if (fields == null) + { + return null; + } + return fields.get(fieldName); + } + + /** * Populates the {@code _File} meta table with initial values. * * @param database @@ -679,9 +719,9 @@ private static void populateFileTable(Database database, Persistence p) { Helper fileHelper = helpers.get(SystemTable._File.shortName); - if (fileHelper == null && log.isLoggable(Level.WARNING)) + if (fileHelper == null && LOG_WARNING) { - if (log.isLoggable(Level.WARNING)) + if (LOG_WARNING) { log.log(Level.WARNING, "Could not obtain the helper class for calling DMO setter methods."); } @@ -725,13 +765,15 @@ { log.log(Level.FINE, "Failed to load " + dmoName, e); } - else if (log.isLoggable(Level.WARNING)) + else if (LOG_WARNING) { log.log(Level.WARNING, "Failed to load " + dmoName + " (set debug level to FINE for stacktrace)"); } return; } + registerWordTables(dmoMeta); + Class dmoImpl = dmoMeta.getImplementationClass(); if (addMetaTableToFile(meta, fileHelper, dmoIface, dmoImpl, p)) @@ -757,6 +799,22 @@ } /** + * Register word tables for a master table. + * + * @param dmoMeta + * DmoMeta for master table + */ + private static void registerWordTables( DmoMeta dmoMeta) + { + Map wt = dmoMeta.getWordTables(); + if (!wt.isEmpty()) + { + WORD_TABLES.computeIfAbsent(dmoMeta.schema, db -> new HashMap<>()). + put(dmoMeta.sqlTable, wt); + } + } + + /** * Add record for a table from the meta database to the _File table. * * @param metaDb @@ -848,7 +906,7 @@ Helper seqHelper = helpers.get(SystemTable._Sequence.shortName); if (seqHelper == null) { - if (log.isLoggable(Level.WARNING)) + if (LOG_WARNING) { log.log(Level.WARNING, "Could not obtain the helper class for calling DMO setter methods."); } @@ -863,7 +921,7 @@ } catch (ClassNotFoundException e) { - if (log.isLoggable(Level.INFO)) + if (LOG_INFO) { log.log(Level.INFO, "No sequences were detected for " + metaDb.getName() + " database."); } @@ -872,7 +930,7 @@ if (!Enum.class.isAssignableFrom(seqEnum)) { - if (log.isLoggable(Level.WARNING)) + if (LOG_WARNING) { log.log(Level.WARNING, "Invalid _Sequences class for " + metaDb.getName() + " database."); } @@ -909,7 +967,7 @@ } catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) { - if (log.isLoggable(Level.WARNING)) + if (LOG_WARNING) { log.log(Level.WARNING, "Failed to populate _Sequence table", e); } @@ -1482,7 +1540,7 @@ continue; } fieldTriggers.add( - new MapBuilder(). + new MapBuilder<>(). put(Session.PK, nextId(metaDb, SystemTable._Field_Trig._file_name)). put("fieldRecid", fieldRecId). put("fileRecid", new integer(_file_num)). @@ -1496,7 +1554,7 @@ else { fileTriggers.add( - new MapBuilder(). + new MapBuilder<>(). put(Session.PK, nextId(metaDb, SystemTable._File_Trig._file_name)). put("fileRecid", new integer(_file_num)). put("event", new character(trigger.event())). @@ -1611,7 +1669,7 @@ boolean charField = CHAR_TYPES.contains(typeName); String charset = charField ? "iso8859-1" : null; String collation = charField ? (caseSensitive ? "basic_S" : "basic_I") : null; - return new MapBuilder(). + return new MapBuilder<>(). put(Session.PK, id). put("fileRecid", new integer(_file_num)). put("fieldName", new character(property.legacy)). @@ -2000,7 +2058,7 @@ this.ifaceName = META_PREFIX + shortName; this._category = _category; this.flags = flags; - this.init = init == null || !log.isLoggable(Level.INFO) ? init : (d, p) -> + this.init = init == null || !LOG_INFO ? init : (d, p) -> { long start = System.nanoTime(); log.log(Level.INFO, String.format("Populating [%s]", _file_name)); @@ -2058,17 +2116,17 @@ { this(_file_num, _file_name, _category, 0, populate); } - + /** * Populate all metatables with provided initializer - * @param metaDb - * meta database - * @param p - * Persistence instance for the meta database + * @param metaDb + * meta database + * @param p + * Persistence instance for the meta database */ public static void populateAll(Database metaDb, Persistence p) { - Arrays.stream(values()).map(t -> t.init).filter(init -> init != null). + Arrays.stream(values()).map(t -> t.init).filter(Objects::nonNull). forEach(init -> init.accept(metaDb, p)); } === modified file 'src/com/goldencode/p2j/persist/meta/TransactionTableUpdater.java' --- src/com/goldencode/p2j/persist/meta/TransactionTableUpdater.java 2020-09-24 17:51:18 +0000 +++ src/com/goldencode/p2j/persist/meta/TransactionTableUpdater.java 2020-10-14 21:06:19 +0000 @@ -4,12 +4,13 @@ ** ** Copyright (c) 2017-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20171030 Created initial version. ** 002 ECF 20180819 Optimized common case of updates at the expense of reads. ** 003 OM 20190122 Rewrote the class to work with new TransactionManager. ** 004 ECF 20200906 New ORM implementation. ** 005 CA 20200924 Replaced Method.invoke with ReflectASM. +** OM 20201012 Force use locally cached meta information instead of map lookup. */ /* @@ -105,8 +106,8 @@ String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaTrans"; try { - implementingClass = DmoMetadataManager.getImplementingClass( - (Class) Class.forName(dmoName)); + DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(Class.forName(dmoName)); + implementingClass = dmoInfo.getImplementationClass(); // map MinimalTrans methods to corresponding dmoClass methods; if we try to use // MinimalTrans methods with a MetaTransImpl object, we will raise === modified file 'src/com/goldencode/p2j/persist/orm/BaseRecord.java' --- src/com/goldencode/p2j/persist/orm/BaseRecord.java 2020-09-22 22:25:18 +0000 +++ src/com/goldencode/p2j/persist/orm/BaseRecord.java 2021-01-07 03:22:58 +0000 @@ -2,12 +2,18 @@ ** Module : BaseRecord.java ** Abstract : The abstract base class for all business data model object classes. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20191001 Created initial version with basic data and accessors. ** OM 20200110 Added support various data types and setter/getter signatures. ** 002 CA 20200922 Added getDatum(offset). +** ECF 20201201 Cleaned up file and added some missing javadoc. +** AIL 20201210 Added bulk set datum for specific buffer-copy operations. +** ECF 20201210 Added direct access to data array for RecordBuffer. +** 20210126 Fixed getUnvalidatedIndices to account optionally for untouched, default property values. +** AIL 20210129 Added support for bulkDataChanged. Improved bulk copy (doing lock and report only once). +** OM 20201027 copy() method will update the status flags to allow the recored to be correctly saved. */ /* @@ -66,6 +72,7 @@ package com.goldencode.p2j.persist.orm; import com.goldencode.p2j.persist.*; +import com.goldencode.p2j.schema.*; import com.goldencode.p2j.util.*; import java.sql.*; import java.util.*; @@ -98,7 +105,7 @@ private final BitSet nullProps; /** Tracker to manage which indices have been updated by property updates */ - private IndexState indexState; + private final IndexState indexState; /** Record identifier (lock variant, which uses table name) for this record (created lazily) */ private RecordIdentifier recid = null; @@ -148,15 +155,26 @@ /** * Copy the source record to the destination record. + *

+ * The records are checked first to decide whether they are compatible (I.e. the number of properties + * matches and also the type for each field, including the optional extent attribute). If not, the method + * returns {@code false} and not copy operation is executed. + *

+ * If the records pass validation, all properties are copied from source ({@code from}) to destination + * {@code to}). At the same time, the status flags are updated so that the destination record can be + * correctly saved. * - * @param from - * The source record. - * @param to - * The destination record. - * - * @return true if the copy was performed (record's tables match). + * @param to + * The destination record. + * @param from + * The source record. + * @param b4reserved + * If {@code true} the before-table specific attributes are copied, too. Otherwise only the normal + * properties are processed. + * + * @return {@code true} if the copy was performed (record's tables match). */ - public static boolean copy(BaseRecord from, BaseRecord to) + public static boolean copy(BaseRecord to, BaseRecord from, boolean b4reserved) { PropertyMeta[] fromProps = from._recordMeta().getPropertyMeta(false); PropertyMeta[] toProps = to._recordMeta().getPropertyMeta(false); @@ -175,11 +193,39 @@ { return false; } - + } + + for (int i = 0; i < fromProps.length; i++) + { + if (!b4reserved) + { + switch (fromProps[i].getId()) + { + case ReservedProperty.ID_ERROR_FLAG: + case ReservedProperty.ID_ERROR_STRING: + case ReservedProperty.ID_ROW_STATE: + case ReservedProperty.ID_ORIGIN_ROWID: + case ReservedProperty.ID_PEER_ROWID: + // skip these attributes/properties + continue; + } + } + // TODO: LOB data is mutable, needs to be duplicated explicitly - to.data[i] = from.data[i]; + boolean change = to.data[i] != from.data[i]; + if (change) + { + to.data[i] = from.data[i]; + + // update the null property bitset and mark the property as changed to be able to save it + to.nullProps.set(i, to.data[i] == null); + to.dirtyProps.set(i); + } } + // mark it as changed + to.updateState(CHANGED, true); + return true; } @@ -323,7 +369,7 @@ int len = this.data.length; this.id = from.id; System.arraycopy(from.data, 0, this.data, 0, len); - dirtyProps.set(0, len); + dirtyProps.set(0, len); // TODO: nullProps? } /** @@ -489,9 +535,14 @@ public String toString() { String sep = System.lineSeparator(); - - StringBuilder buf = new StringBuilder(); int len = data.length; + + StringBuilder buf = new StringBuilder(_recordMeta().tables[0]); + buf.append(':').append(primaryKey()).append(sep) + .append(state).append(sep) + .append("dirty: ").append(dirtyProps).append(sep) + .append("unvalidated: ").append(unvalidated).append(sep) + .append("data: {"); for (int i = 0; i < len; i++) { if (i > 0) @@ -499,14 +550,11 @@ buf.append(", "); } - buf.append(String.valueOf(data[i])); + buf.append(data[i]); } - return _recordMeta().tables[0] + ':' + primaryKey() + sep + - state + sep + - "dirty: " + dirtyProps + sep + - "unvalidated: " + unvalidated + sep + - "data: {" + buf.toString() + "}"; + buf.append('}'); + return buf.toString(); } /** @@ -526,7 +574,7 @@ * @throws NullPointerException * If the passed in parameter is {@code null}. */ - public Object[] getData(com.goldencode.p2j.schema.PropertyMapper pm) + public Object[] getData(PropertyMapper pm) { Objects.requireNonNull(pm); @@ -534,6 +582,26 @@ } /** + * This accessor allows direct access to private {@code data} array only to + * instances of {@code RecordBuffer}. + * + * @param rb + * The {@code RecordBuffer} which requests access. + * + * @return The reference to internal {@code data} array if the passed parameter is a + * {@code RecordBuffer} + * + * @throws NullPointerException + * If the passed in parameter is {@code null}. + */ + public Object[] getData(RecordBuffer rb) + { + Objects.requireNonNull(rb); + + return data; + } + + /** * Indicate whether any of this record's validatable properties are dirty and have not been validated. * * @return {@code true} if record needs validation, else {@code false}. @@ -544,6 +612,72 @@ } /** + * Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by + * the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit + * corresponds with the 0-based position of the associated index in the array of unique or non-unique + * indices for the record. + * + * @param dirtyOffset + * Optional offset of a dirty property which must be a component of the indices returned. If + * less than 0, all dirty properties are considered. If a non-negative offset is provided, + * but the corresponding property is not dirty, an empty bit set is returned. + * @param unique + * {@code true} to return dirty unique indices; {@code false} to return dirty non-unique indices. + * @param remaining + * If {@code true}, only unvalidated indices are returned. Otherwise, validation is not + * considered. + * + * @return A bit set, as described above. + */ + public BitSet getDirtyIndices(int dirtyOffset, boolean unique, boolean remaining) + { + RecordMeta recMeta = _recordMeta(); + BitSet filter = new BitSet(); + BitSet[] indices = unique ? recMeta.uniqueIndices : recMeta.nonuniqueIndices; + + if (remaining) + { + filter.or(indexState.getBitSet(unique)); + } + else + { + filter.set(0, indices.length); + } + + return getDirtyIndices(indices, dirtyOffset, filter, (state.state & NEW) == NEW); + } + + /** + * Get a two element array representing the old and new values of the property currently being changed. + * The first element is the previous value of the property; the second is the new value. If the property + * has not been touched or the old and new values are the same, {@code null} is returned. + *

+ * Note: this method will only return a non-null result while a property update is + * active; that is, while a DMO property is being updated via the {@code RecordBuffer} invocation handler. + * It is only at that moment that the previous value for the property being changed is guaranteed to + * contain meaningful data. + * + * @return See above. + */ + public Object[] getActiveUpdateDiffs() + { + if (activeOffset < 0 || + !dirtyProps.get(activeOffset) || + Objects.equals(activePropPreviousValue, data[activeOffset])) + { + return null; + } + + return new Object[] { activePropPreviousValue, data[activeOffset] }; + } + + // TODO: making this public. should return a copy of it? + public BitSet getDirtyProps() + { + return dirtyProps; + } + + /** * Set a single datum into the data array, and update the record and property state accordingly. * * @param offset @@ -590,6 +724,122 @@ } /** + * Set all datum in the data array. This works only if the source has the same structure as the + * destination (this). A prevalidation should be done - this will only check integrity, type and extents. + * The order of the matching will be from left to right and it will avoid reserved properties. + * + * + * @param source + * A record from which the data will be copied to this data array. + * + * @return {@code true} if a valid copying could be done; so the properties match in type and extent and + * the number of properties match (except the reserved ones). + */ + protected boolean setAllDatum(BaseRecord source) + { + PropertyMeta[] fromProps = source._recordMeta().getPropertyMeta(false); + PropertyMeta[] toProps = this._recordMeta().getPropertyMeta(false); + Map mapping = new HashMap<>(); + int i, j; + + // do the mapping and check if this copy makes sense (we have a correct mapping) + for (i = 0, j = 0; i < fromProps.length && j < toProps.length;) + { + PropertyMeta fromProp = fromProps[i]; + PropertyMeta toProp = toProps[j]; + if (fromProp.isReserved()) + { + i++; + continue; + } + if (toProp.isReserved()) + { + j++; + continue; + } + + if (fromProp.getType() != toProp.getType() || + fromProp.getExtent() != toProp.getExtent()) + { + return false; + } + + mapping.put(j, i); + j++; + i++; + } + + // the mapping is broken; the source can't be copied here so this method was used incorrectly + if (!(i == fromProps.length && j == toProps.length)) + { + return false; + } + + // do an initial filtering of the offsets which should be updated + List updatedOffsets = null; + List updatedDatums = null; + for (Map.Entry entry : mapping.entrySet()) + { + Integer toOffset = entry.getKey(); + Integer fromOffset = entry.getValue(); + Object datum = source.getDatum(fromOffset); + + // update the bitset of dirty properties + dirtyProps.set(toOffset); + + // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the + // same, regardless of the scale value. + if (!(Objects.equals(data[toOffset], datum) || + (datum instanceof Comparable && + data[toOffset] != null && + ((Comparable) datum).compareTo(data[toOffset]) == 0))) + { + if (updatedOffsets == null) + { + updatedOffsets = new ArrayList<>(); + updatedDatums = new ArrayList<>(); + } + + updatedOffsets.add(toOffset); + updatedDatums.add(datum); + } + } + + // we shouldn't do any copy as no offset will be updated + if (updatedOffsets == null || updatedOffsets.size() == 0) + { + return true; + } + + // we will do a copy, so upgrade lock at first + if (!lockForUpdate()) + { + return false; + } + + try + { + // notify data change in bulk mode + // NOTE: this does not set activeOffset as we try to update all offsets at once! + bulkDataChanged(updatedOffsets, updatedDatums); + } + catch (PersistenceException exc) + { + throw new RuntimeException(exc); + } + + // finally, do the data set + for (int idx = 0; idx < updatedOffsets.size(); idx++) + { + int offset = updatedOffsets.get(idx); + Object datum = updatedDatums.get(idx); + data[offset] = datum; + } + + return true; + } + + /** * Get the field's value on the specified offset. * * @param offset @@ -650,96 +900,6 @@ } /** - * Get a bit set, the set bits of which represent the positions of fully dirty unique indices in the - * array of this DMO's unique indices. If no unique indices exist, or if none are fully dirty (i.e., - * all fields in the index have been touched), no bits will be set in the returned bit set. - * - * @param dirtyOffset - * If non-negative, the position in the data array of a property which must be included in any - * dirty index included in the result. - * - * @return A bit set, as described above. - */ - /* - BitSet getFullyDirtyUniqueIndices(int dirtyOffset) - { - return getFullyDirtyIndices(_recordMeta().uniqueIndices, dirtyOffset); - } - */ - - /* - BitSet getFullyDirtyUniqueIndices(int dirtyOffset, boolean open) - { - return getFullyDirtyIndices(_recordMeta().uniqueIndices, dirtyOffset, open); - } - */ - - /** - * Get a bit set, the set bits of which represent the positions of fully dirty nonunique indices in the - * array of this DMO's nonunique indices. If no nonunique indices exist, or if none are fully dirty - * (i.e., all fields in the index have been touched), no bits will be set in the returned bit set. - * - * @param dirtyOffset - * If non-negative, the position in the data array of a property which must be included in any - * dirty index included in the result. - * - * @return A bit set, as described above. - */ - /* - BitSet getFullyDirtyNonuniqueIndices(int dirtyOffset) - { - return getFullyDirtyIndices(_recordMeta().nonuniqueIndices, dirtyOffset); - } - */ - - /** - * Get a bit set, the set bits of which represent the positions of fully dirty indices in the given - * array of indices. If {@code indices} is empty, or if none of the indices in the array are fully - * dirty (i.e., all fields in the index have been touched), no bits will be set in the returned bit set. - * - * @param indices - * Array of bit sets, each representing the positions of indexed fields in the DMO's data array. - * @param dirtyOffset - * If non-negative, the position in the data array of a property which must be included in any - * dirty index included in the result. - * - * @return A bit set, as described above. - */ - /* - BitSet getFullyDirtyIndices(BitSet[] indices, int dirtyOffset) - { - BitSet dirty = new BitSet(); - - if (dirtyOffset >= 0 && !dirtyProps.get(dirtyOffset)) - { - // property at offset is not dirty, so there can be no fully dirty indices containing the given - // property - return dirty; - } - - int len = indices.length; - for (int i = 0; i < len; i++) - { - BitSet orig = indices[i]; - - if (dirtyOffset >= 0 && !orig.get(dirtyOffset)) - { - continue; - } - - BitSet copy = (BitSet) orig.clone(); - copy.and(dirtyProps); - if (copy.equals(orig)) - { - dirty.set(i); - } - } - - return dirty; - } - */ - - /** * Mark all dirty properties as having been validated. */ void markValidated() @@ -761,24 +921,29 @@ /** * Get a bit set representing the unique indices, if any, which are either fully or partially updated * and which have not already been validated. If the record is newly created and has not yet been - * flushed to the database, a fully updated index is required; otherwise, a partialy updated index is + * flushed to the database, a fully updated index is required; otherwise, a partially updated index is * sufficient. * * @param dirtyOffset * Optional offset of a dirty property which must be a component of the indices returned. If * less than 0, all dirty properties are considered. If a non-negative offset is provided, * but the corresponding property is not dirty, an empty bit set is returned. + * @param allUnvalidated + * If {@code true}, then any unique index containing any property not previously validated is + * included in the returned bit set. If {@code false}, then only indices containing properties + * that have been explicitly made dirty are included. Setting this to {@code true} effectively + * considers all properties with untouched, default values in the check. * * @return A bit set as described above. */ - BitSet getUnvalidatedIndices(int dirtyOffset) + BitSet getUnvalidatedIndices(int dirtyOffset, boolean allUnvalidated) { BitSet result = new BitSet(); // nothing to do if: // * there are no dirty properties; or // * the given dirty offset does not represent an unvalidated property - if (dirtyProps.isEmpty() || (dirtyOffset >= 0 && !unvalidated.get(dirtyOffset))) + if (!allUnvalidated && (dirtyProps.isEmpty() || (dirtyOffset >= 0 && !unvalidated.get(dirtyOffset)))) { return result; } @@ -791,6 +956,18 @@ { BitSet nextUnique = unique[i]; + if (allUnvalidated) + { + // if the unique index contains any unvalidated property (including untouched default values), + // include it in the set of indices which require validation + if (nextUnique.intersects(unvalidated)) + { + result.set(i); + } + + continue; + } + // skip this one if: // * an offset is provided, but it is not a component of the index; or // * no offset is provided, but none of the unvalidated properties are a component of the index @@ -821,55 +998,6 @@ return result; } - private BitSet getDirtyIndices(BitSet[] indices, int dirtyOffset, BitSet filter, boolean full) - { - BitSet dirty = new BitSet(); - - if (filter.isEmpty() || (dirtyOffset >= 0 && !dirtyProps.get(dirtyOffset))) - { - // filter is set to filter ALL indices OR property at offset is not dirty, so there can be no - // fully dirty indices containing the given property - return dirty; - } - - for (int i = filter.nextSetBit(0); i >= 0; i = filter.nextSetBit(i + 1)) - { - BitSet orig = indices[i]; - - if (dirtyOffset >= 0 && !orig.get(dirtyOffset)) - { - continue; - } - - BitSet copy = (BitSet) orig.clone(); - copy.and(dirtyProps); - if (full ? copy.equals(orig) : !copy.isEmpty()) - { - dirty.set(i); - } - } - - return dirty; - } - - public BitSet getDirtyIndices(int dirtyOffset, boolean unique, boolean remaining) - { - RecordMeta recMeta = _recordMeta(); - BitSet filter = new BitSet(); - BitSet[] indices = unique ? recMeta.uniqueIndices : recMeta.nonuniqueIndices; - - if (remaining) - { - filter.or(indexState.getBitSet(unique)); - } - else - { - filter.set(0, indices.length); - } - - return getDirtyIndices(indices, dirtyOffset, filter, (state.state & NEW) == NEW); - } - /** * Get the offset of the property currently being updated, if any. When a DMO's setter method * is invoked, the DMO goes into an 'active' state, during which the offset of the @@ -888,36 +1016,6 @@ } /** - * Get a two element array representing the old and new values of the property currently being changed. - * The first element is the previous value of the property; the second is the new value. If the property - * has not been touched or the old and new values are the same, {@code null} is returned. - *

- * Note: this method will only return a non-null result while a property update is - * active; that is, while a DMO property is being updated via the {@code RecordBuffer} invocation handler. - * It is only at that moment that the previous value for the property being changed is guaranteed to - * contain meaningful data. - * - * @return See above. - */ - public Object[] getActiveUpdateDiffs() - { - if (activeOffset < 0 || - !dirtyProps.get(activeOffset) || - Objects.equals(activePropPreviousValue, data[activeOffset])) - { - return null; - } - - return new Object[] { activePropPreviousValue, data[activeOffset] }; - } - - // TODO: making this public. should return a copy of it? - public BitSet getDirtyProps() - { - return dirtyProps; - } - - /** * Reset record and property state after database is synchronized with the record's data * (i.e., the most recent set of changes have been persisted). *

@@ -1068,6 +1166,62 @@ } /** + * Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by + * the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit + * corresponds with the 0-based position of the associated index in the array of unique or non-unique + * indices for the record. + * + * @param indices + * The array of bit sets representing the indices to be checked. This must be either the set of + * unique indices or non-unique indices associated with the record. Only one or the other can be + * checked by a call to this method. + * @param dirtyOffset + * Optional offset of a dirty property which must be a component of the indices returned. If + * less than 0, all dirty properties are considered. If a non-negative offset is provided, + * but the corresponding property is not dirty, an empty bit set is returned. + * @param filter + * An inclusive filter to apply to the results. If non-empty, each bit in the filter's bit set + * represents the position of an index to be checked in the {@code indices} parameter. If empty, + * the returned bit set will be empty. + * @param full + * If {@code true}, ALL index components must be dirty for an index to be considered dirty. + * If {@code false}, one or more index components must be dirty for an index to be considered + * dirty. + * + * @return A bit set, as described above. + */ + private BitSet getDirtyIndices(BitSet[] indices, int dirtyOffset, BitSet filter, boolean full) + { + BitSet dirty = new BitSet(); + + if (filter.isEmpty() || (dirtyOffset >= 0 && !dirtyProps.get(dirtyOffset))) + { + // filter is set to filter ALL indices OR property at offset is not dirty, so there can be no + // fully dirty indices containing the given property + return dirty; + } + + for (int i = filter.nextSetBit(0); i >= 0; i = filter.nextSetBit(i + 1)) + { + BitSet orig = indices[i]; + + if (dirtyOffset >= 0 && !orig.get(dirtyOffset)) + { + continue; + } + + BitSet copy = (BitSet) orig.clone(); + copy.and(dirtyProps); + if (full ? copy.equals(orig) : !copy.isEmpty()) + { + dirty.set(i); + } + } + + return dirty; + } + + /** * A property's value has changed. Update the record's internal state accordingly and set up rollback * information if the record is undoable. * @@ -1108,6 +1262,57 @@ } /** + * Multiple property values have changed. Update the record's internal state accordingly and set up rollback + * information if the record is undoable. + * + * @param offsets + * A list of offsets for the changed values in record's data array. + * @param datums + * A list of new values for the changed datums. + */ + private void bulkDataChanged(List offsets, List datums) + throws PersistenceException + { + if (activeBuffer == null) + { + return; + } + + for (int i = 0; i < offsets.size(); i++) + { + int offset = offsets.get(i); + Object datum = datums.get(i); + + // update the unvalidated properties bitset + if (_recordMeta().validatable.get(offset)) + { + unvalidated.set(offset); + } + + // update the null property bitset + nullProps.set(offset, datum == null); + + // this doesn't make sense as there is no activeOffset set + // activePropPreviousValue = data[offset]; + } + + int flags = CHANGED /*| NEEDS_VALIDATION*/; + updateState(flags, true); + + int txLevel = prepareChangeSet(activeBuffer.getSession()); + if (txLevel >= 0) + { + changeSet.logStateChange(flags, txLevel); + for (int i = 0; i < offsets.size(); i++) + { + int offset = offsets.get(i); + Object datum = datums.get(i); + changeSet.logDataChange(txLevel, offset, datum); + } + } + } + + /** * Updates the set of state flags for this record. Depending on {@code add} parameter, the * flags can be set (when {@code true}) or removed (when {@code false}. If one flag is already * set and {@code add == true} that flag remains unchanged. The same stands for un-setting a @@ -1189,6 +1394,13 @@ return txLevel; } + /** + * Obtain an exclusive lock for the current record on behalf of the active buffer, if any. + * + * @return {@code true} if the lock is successfully obtained, or if it is unneeded due to system state, + * or if there is no active buffer. {@code false} if there was an error obtaining the lock (and + * we are in silent error mode). + */ private boolean lockForUpdate() { if (DatabaseManager.isInitializing()) @@ -1222,6 +1434,11 @@ } } + /** + * Get the name of the primary table associated with this record. + * + * @return Primary table name. + */ private String getTable() { String[] tables = _recordMeta().tables; === modified file 'src/com/goldencode/p2j/persist/orm/DDLGeneratorWorker.java' --- src/com/goldencode/p2j/persist/orm/DDLGeneratorWorker.java 2020-09-25 16:00:36 +0000 +++ src/com/goldencode/p2j/persist/orm/DDLGeneratorWorker.java 2021-01-29 19:50:19 +0000 @@ -9,6 +9,7 @@ ** 001 OM 20191009 Created initial version. Supported permanent tables and indexes DDLs. ** 20200618 Sorted tables alphabetically. Unique indexes are generated first. ** 002 OM 20200924 Index components carry multiple information to avoid map lookups for them. +** 003 IAS 20201203 Added generation database object for the word tables' support. */ /* @@ -66,6 +67,16 @@ package com.goldencode.p2j.persist.orm; +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.function.*; +import java.util.logging.*; +import java.util.stream.*; + +import org.apache.commons.collections.map.*; + +import com.goldencode.ast.*; import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.convert.*; import com.goldencode.p2j.pattern.*; @@ -75,11 +86,6 @@ import com.goldencode.p2j.schema.*; import com.goldencode.p2j.util.*; -import java.io.*; -import java.nio.file.*; -import java.util.*; -import java.util.logging.*; - /** * This worker gathers schema information from TRPL and constructs DDL statements for initializing * the permanent database (tables, indexes and sequences). @@ -122,8 +128,11 @@ */ public static final String DTZ_OFFSET = "_offset"; + /** Database objects' names parts' separator */ + public static final String NAME_PARTS_SEPARATOR = "__"; + /** Fixed size space used to make the output pretty. */ - private final String INDENT = " "; + private static final String INDENT = " "; /** * The constructor configures the supporting library with exported functionality. @@ -134,6 +143,261 @@ } /** + * Get the schema name based on database name. + * + * @param dbName + * The database name. + * + * @return The schema name. + */ + private String getSchemaName(String dbName) + { + if (MetadataManager.META_SCHEMA.equals(dbName)) + { + return Configuration.getSchemaConfig().getMetadata().getName(); + } + return dbName; + } + + /** Word table data */ + public static class WordTable + { + /** parent table name */ + public final String parentTableName; + /** field name */ + public final String fieldName; + /** extent size. */ + public final long extent; + /** field is case-sensitive flag */ + public final boolean caseSensitive; + /** word table name */ + public final String tableName; + /** word table PK name */ + public final String pkName; + /** word table FK index name */ + public final String fkIndexName; + /** word table FK constraint name */ + public final String fkName; + /** word table index name */ + public final String indexName; + /** word table trigger function name */ + public final String triggerFunctionName; + /** word table AFTER UPDATE trigger name */ + public final String afterUpdateTriggerName; + /** word table AFTER INSERT trigger name */ + public final String afterInsertTriggerName; + + /** + * Constructor + * + * @param parentTableName + * parent table name + * @param fieldName + * field name + * @param extent + * field extent + * @param caseSensitive + * field is case-sensitive flag + * @param tableName + * word table name + * @param pkName + * word table PK name + * @param fkIndexName + * word table FK index name + * @param fkName + * word table FK constraint name + * @param indexName + * word table index name + * @param triggerFunctionName + * word table trigger function name + * @param afterUpdateTriggerName + * word table AFTER UPDATE trigger name + * @param afterInsertTriggerName + * word table AFTER INSERT trigger name + */ + public WordTable( + String parentTableName, String fieldName, long extent, boolean caseSensitive, + String tableName, String pkName, String fkIndexName, String fkName, String indexName, + String triggerFunctionName, + String afterUpdateTriggerName, String afterInsertTriggerName) + { + this.parentTableName = parentTableName; + this.fieldName = fieldName; + this.extent = extent; + this.caseSensitive = caseSensitive; + this.tableName = tableName; + this.pkName = pkName; + this.fkIndexName = fkIndexName; + this.fkName = fkName; + this.indexName = indexName; + this.triggerFunctionName = triggerFunctionName; + this.afterUpdateTriggerName = afterUpdateTriggerName; + this.afterInsertTriggerName = afterInsertTriggerName; + } + /** Wird table data builder */ + public static class Builder + { + /** parent table name */ + private final String parentTableName; + /** field name */ + private final String fieldName; + /** extent size. */ + private final long extent; + /** field is case-sensitive flag */ + private final boolean caseSensitive; + /** word table name */ + private String tableName; + /** word table PK name */ + private String pkName; + /** word table FK index name */ + private String fkIndexName; + /** word table FK constraint name */ + private String fkName; + /** word table index name */ + private String indexName; + /** word table trigger function name */ + private String triggerFunctionName; + /** word table AFTER INSERT trigger name */ + private String afterInsertTriggerName; + /** word table AFTER UPDATE trigger name */ + private String afterUpdateTriggerName; + + /** + * Constructor + * + * @param nb + * The database objects' name builder. + * @param parentTableName + * parent table name + * @param fieldName + * field name + * @param extent + * field extent + * @param caseSensitive + * field is case-sensitive flag + */ + public Builder(NameBuilder nb, + String parentTableName, String fieldName, long extent, boolean caseSensitive) + { + this.parentTableName = extent == 0 ? parentTableName : parentTableName + "__" + extent; + this.fieldName = fieldName; + this.extent = extent; + this.caseSensitive = caseSensitive; + this.tableName = nb.build(parentTableName, fieldName); + this.pkName = nb.build("pk", parentTableName, fieldName); + this.fkIndexName = nb.build("fkidx", parentTableName, fieldName); + this.fkName = nb.build("fk", parentTableName, fieldName); + this.indexName = nb.build("idx", parentTableName, fieldName); + this.triggerFunctionName = nb.build(parentTableName, fieldName, "trg"); + this.afterInsertTriggerName = nb.build(parentTableName, fieldName, "ins"); + this.afterUpdateTriggerName = nb.build(parentTableName, fieldName, "upd"); + } + + /** + * Set word table name. + * + * @param value + * word table name. + * @return this Builder instance + */ + public Builder tableName(String value) + { + this.tableName = value; + return this; + } + + /** + * Set word table FK index name. + * + * @param value + * word table FK index name. + * @return this Builder instance + */ + public Builder fkIndexName(String value) + { + this.fkIndexName = value; + return this; + } + + /** + * Set word table FK constraint name. + * + * @param value + * word table FK constraint name. + * @return this Builder instance + */ + public Builder fkName(String value) + { + this.fkName = value; + return this; + } + + /** + * Set word table index name. + * + * @param value + * word table index name. + * @return this Builder instance + */ + public Builder indexName(String value) + { + this.indexName = value; + return this; + } + + /** + * Set word table trigger function name. + * + * @param value + * word table trigger function name, + * @return this Builder instance + */ + public Builder triggerFunctionName(String value) + { + this.triggerFunctionName = value; + return this; + } + + /** + * Set word table AFTER INSERT trigger name. + * + * @param value + * word table AFTER INSERT trigger name. + * @return this Builder instance + */ + public Builder afterInsertTriggerName(String value) + { + this.afterInsertTriggerName = value; + return this; + } + + /** + * Set word table AFTER UPDATE trigger name. + * + * @param value + * word table AFTER UPDATE trigger name. + * @return this Builder instance + */ + public Builder afterUpdateTriggerName(String value) + { + this.afterUpdateTriggerName = value; + return this; + } + + /** + * Build word table data + * @return word table data + */ + public WordTable build() + { + return new WordTable(parentTableName, fieldName, extent, caseSensitive, + tableName, pkName, fkIndexName, fkName, indexName, + triggerFunctionName, afterUpdateTriggerName, afterInsertTriggerName); + } + } + + } + /** * Dialect-independent data structure class for SQL tables. Holds all needed information * related to a SQL table (name, fields, indexes). The structure is filled in a granular * fashion and iterated when the DDL artifacts are created (table and index DDLs). @@ -232,7 +496,19 @@ this.cycle = cycle; } } - + + /** Database object names' builder */ + private static interface NameBuilder + { + /** + * Construct database object name concatenating parts + * + * @param parts + * parts of the name + * @return object name + */ + public String build(String... parts); + } /** * The class that exposes its public methods as worker interface in TRPL. The internal data * is acquired piece by piece using {@code add***()} methods. When the entire schema @@ -241,9 +517,21 @@ */ public class Helper { + /** Max length of the database object name (most restrictive limit as for PostgreSQL)*/ + private static final int MAX_OBJECT_NAME_LEN = 63; + /** The set of all tables mapped by their schema. */ private final Map> tables = new TreeMap<>(); + /** The set of all objects' names generated for word tables by mapped by their schema. */ + private final Map> wordTablesNames = new TreeMap<>(); + + /** Word tables by name */ + private final Map wordTables = new HashMap<>(); + + /** Word tables' keys DDLs by word table name */ + private final Map> wordTablesKeys = new HashMap<>(); + /** The set of all sequences mapped by their schema. */ private final Map> sequences = new HashMap<>(); @@ -251,6 +539,14 @@ private final P2JField ID = new P2JField(PK, ParmType.INT64, 0L, null, null, null, null, false, null, null, false, null, null, null, null, true, 0); + /** 'parent' field of the word table */ + private final P2JField PARENT = new P2JField("parent__id", ParmType.INT64, 0L, + null, null, null, null, false, null, null, false, null, null, null, null, true, 0); + + /** 'word' field of the word table */ + private final P2JField WORD = new P2JField("word", ParmType.CHAR, 0L, + null, null, null, null, false, null, null, false, null, null, null, null, true, 0); + /** * This column is used as foreign key in a normalized composite table. The value refer to * the record to which the extent components belongs to. @@ -414,6 +710,50 @@ } /** + * Get the name of the word table for the word index + * + * @param dbName + * The target schema. + * @param tableName + * The target (legacy) table name . + * @param indexName + * The legacy name of the index. + * + * @return the name of the word table for the word index + */ + public String getWordTableName(String dbName, String tableName, String indexName) + { + if ("_temp".equals(dbName)) + { + return ""; + } + Map tbl = tables.get(dbName); + if (tbl == null) + { + throw new IllegalArgumentException("Unknown database: [" + dbName + "]"); + } + Table table = tbl.get(tableName); + if (table == null) + { + throw new IllegalArgumentException("Unknown table: [" + tableName + "]"); + } + P2JIndex index = table.indexes.get(indexName); + if (index == null) + { + throw new IllegalArgumentException("Unknown index: [" + indexName + "]"); + } + if (!index.isWord()) + { + throw new IllegalArgumentException("Not a word index: [" + indexName + "]"); + } + P2JIndexComponent component = index.components().next(); + Map wordTableNames = new HashedMap(); + String fieldName = component.getColumnName(); + String wordTableName = createDbObjectName(dbName, tableName, fieldName); + index.setWordTableName(wordTableName); + return wordTableName; + } + /** * Adds a new index to an existing table (added previously with {@code addTable()}). In * case of validation error the method logs the event and returns without adding the index. * @@ -631,54 +971,88 @@ ddlFolder.mkdirs(); } - SchemaConfig schemaConfig = Configuration.getSchemaConfig(); String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys")) - ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; - String dialectStr = schemaConfig.getNamespaceParameter(dbName, "ddl-dialects"); - String[] dialects = dialectStr == null ? new String[] { "h2" } : dialectStr.split(","); - - for (String dialectId : dialects) - { - Class dialectCls = DialectHelper.getDialectClass(dialectId); - if (dialectCls != null) - { - try - { - Dialect dialect = dialectCls.newInstance(); - String fileName = "schema_table_" + getSchemaName(dbName) + "_" + dialectId + ".sql"; - File outFile = new File(ddlFolder, fileName); - PrintStream out = new PrintStream(outFile); - String coll = schemaConfig.getNamespaceParameter(dbName, dialectId + "/collation"); - if (coll != null && dialect.explicitSetCollation()) - { - out.print("set collation " + coll + dialect.getDelimiter() + eoln); - } - List preps = dialect.getDatabasePrepareStatements(new Database(dbName)); - if (preps != null) - { - for (String prep : preps) - { - out.print(prep + eoln); - } - out.print(eoln); - } - generateTableDDLImpl(dbName, dialect, out, eoln); - out.close(); // make sure the file is closed - - if (MetadataManager.META_SCHEMA.equals(dbName)) - { - copySqlFile(outFile, MetadataManager.DDL_TABLE); - } - } - catch (InstantiationException | IllegalAccessException | IOException e) - { - e.printStackTrace(); - } - } - } + ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; + forAllDialects(dbName, dialect -> { + String fileName = "schema_table_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql"; + File outFile = new File(ddlFolder, fileName); + try + { + PrintStream out = new PrintStream(outFile); + String coll = Configuration.getSchemaConfig(). + getNamespaceParameter(dbName, dialect.id() + "/collation"); + if (coll != null && dialect.explicitSetCollation()) + { + out.print("set collation " + coll + dialect.getDelimiter() + eoln); + } + List preps = dialect.getDatabasePrepareStatements(new Database(dbName)); + if (preps != null) + { + for (String prep : preps) + { + out.print(prep + eoln); + } + out.print(eoln); + } + generateTableDDLImpl(dbName, dialect, out, eoln); + out.close(); // make sure the file is closed + + if (MetadataManager.META_SCHEMA.equals(dbName)) + { + copySqlFile(outFile, MetadataManager.DDL_TABLE); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + }); } /** + * Generate triggers' DDL to support word tables. + * @param dbName + * The name of the schema to be processed. + */ + public void generateWordTablesDDLs(String dbName) + { + if (DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName) || + !tables.containsKey(dbName)) + { + // quick out. Do not process _temp or other undefined schema. + return; + } + + // make sure the ddl folder exists: + File ddlFolder = new File("ddl"); + if (!ddlFolder.exists()) + { + ddlFolder.mkdirs(); + } + + String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys")) + ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; + forAllDialects(dbName, dialect -> { + String fileName = "schema_word_tables_" + getSchemaName(dbName) + + "_" + dialect.id() + ".sql"; + try (PrintStream out = new PrintStream(new File(ddlFolder, fileName))) + { + dialect.generateWordTablesDDLImpl(dbName, out, eoln, wordTables); + wordTablesKeys.values().stream().flatMap(List::stream).forEach(s -> { + out.printf(s); + out.print(eoln); + out.print(eoln); + }); + } + catch (IOException e) + { + e.printStackTrace(); + } + + }); + } + + /** * Generates the DDLs needed to create all sequences in a database with a specific schema. * The sequence DDL statements are APPENDED at the end of table DDL definitions, for each * dialect (i.e. {@code ddl/schema_table__.sql}). @@ -701,33 +1075,21 @@ { ddlFolder.mkdirs(); } - - SchemaConfig schemaConfig = Configuration.getSchemaConfig(); String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys")) - ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; - String dialectStr = schemaConfig.getNamespaceParameter(dbName, "ddl-dialects"); - String[] dialects = dialectStr == null ? new String[] { "h2" } : dialectStr.split(","); - - for (String dialectId : dialects) - { - Class dialectCls = DialectHelper.getDialectClass(dialectId); - if (dialectCls != null) - { - try - { - Dialect dialect = dialectCls.newInstance(); - String fileName = - "schema_table_" + getSchemaName(dbName) + "_" + dialectId + ".sql"; - PrintStream out = new PrintStream( - new FileOutputStream(new File(ddlFolder, fileName), true)); - generateSequenceDDLImpl(dbName, dialect, out, eoln); - } - catch (InstantiationException | IllegalAccessException | FileNotFoundException e) - { - e.printStackTrace(); - } - } - } + ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; + forAllDialects(dbName, dialect -> { + String fileName = + "schema_table_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql"; + try(PrintStream out = new PrintStream( + new FileOutputStream(new File(ddlFolder, fileName), true))) + { + generateSequenceDDLImpl(dbName, dialect, out, eoln); + } + catch (FileNotFoundException e) + { + e.printStackTrace(); + } + }); } /** @@ -758,6 +1120,37 @@ preprocessIndices(dbName); + String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys")) + ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; + forAllDialects(dbName, dialect -> { + String fileName = "schema_index_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql"; + File outFile = new File(ddlFolder, fileName); + try + { + PrintStream out = new PrintStream(outFile); + generateIndexDDLImpl(dbName, dialect, out, eoln); + out.close(); + if (MetadataManager.META_SCHEMA.equals(dbName)) + { + copySqlFile(outFile, MetadataManager.DDL_INDEX); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + }); + } + + /** + * Do a job for all dialects + * @param dbName + * database name + * @param worker + * job worker + */ + private void forAllDialects(String dbName, Consumer worker) + { SchemaConfig schemaConfig = Configuration.getSchemaConfig(); String eoln = EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys")) ? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */; @@ -772,25 +1165,15 @@ try { Dialect dialect = dialectCls.newInstance(); - String fileName = "schema_index_" + getSchemaName(dbName) + "_" + dialectId + ".sql"; - File outFile = new File(ddlFolder, fileName); - PrintStream out = new PrintStream(outFile); - generateIndexDDLImpl(dbName, dialect, out, eoln); - out.close(); // make sure the file is closed - - if (MetadataManager.META_SCHEMA.equals(dbName)) - { - copySqlFile(outFile, MetadataManager.DDL_INDEX); - } + worker.accept(dialect); } - catch (InstantiationException | IllegalAccessException | IOException e) + catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } } } } - /** * Preprocesses the indexes, dropping the UNIQUE option for those that are implicitly unique due to * a previous index having the same components has already been validated as unique. @@ -816,7 +1199,10 @@ Set> indexEntries = table.indexes.entrySet(); for (Map.Entry indexEntry : indexEntries) { - idxList.add(indexEntry.getValue()); + if (!indexEntry.getValue().isWord()) + { + idxList.add(indexEntry.getValue()); + } } // do the processing: @@ -994,7 +1380,7 @@ addDDLs.add(fkIndex); // add the constraint; the name is the FK prefix followed by the composite table name - String fk = "alter table " + compositeTableName + eoln + + String fk = "alter TABLE " + compositeTableName + eoln + INDENT + "add constraint " + "FK_" + compositeTableName.toUpperCase() + eoln + INDENT + "foreign key (parent__id)" + eoln + INDENT + "references " + table.name + eoln + @@ -1029,6 +1415,11 @@ out.print(eoln); out.print(eoln); } + + if (!dialect.useUdf4Contains()) + { + generateWordTable(dbName, table, out, dialect, eoln); + } } for (String addDLL : addDDLs) @@ -1039,7 +1430,93 @@ out.print(eoln); } } - + + /** + * Generate auxiliary word table + * + * @param dbName + * The target schema. + * @param table + * parent table + * @param out + * The {@code PrintStream} used for output. + * @param dialect + * The {@code Dialect} to be used. + * @param eoln + * OS-specific end of line terminator. + * @param addDDLs + * indexes holder + */ + private void generateWordTable(String dbName, + Table table, + PrintStream out, + Dialect dialect, + String eoln) + { + List wordIndexes = table.indexes.values(). + stream().filter(P2JIndex::isWord).collect(Collectors.toList()); + for (P2JIndex wordIndex: wordIndexes) + { + String tableName = wordIndex.getTable(); + Iterator components = wordIndex.components(); + if (!components.hasNext()) + { + continue; + } + P2JIndexComponent indexComponent = components.next(); + String fieldName = indexComponent.getColumnName(); + long extent = table.fields.get(fieldName).getExtent(); + boolean caseSensitive = !indexComponent.isIgnoreCase(); + WordTable.Builder builder = new WordTable.Builder( + parts -> createDbObjectName(dbName, parts), + tableName, fieldName, extent, caseSensitive); + // TODO: use hints to override default objects' names + WordTable wordTable = builder.build(); + Table wtable = new Table(wordTable.tableName); + wordTables.put(wordTable.tableName, wordTable); + StringBuilder wt = new StringBuilder(); + wt.append(dialect.getCreateTableString()). + append(wordTable.tableName). + append(" (").append(eoln); + addFieldImpl(PARENT, dialect, wtable, wt, eoln); + if (extent > 0) + { + addFieldImpl(list__index, dialect, wtable, wt, eoln); + } + addLastFieldImpl(WORD, dialect, wtable, wt, eoln); + wt.append(")"); + out.print(wt.toString()); + out.print(dialect.getDelimiter()); + out.print(eoln); + out.print(eoln); + List keys = new ArrayList<>(); + + // add the pk constraint; + String pk = "alter table " + wordTable.tableName+ eoln + + INDENT + "add constraint " + wordTable.pkName + eoln + + INDENT + "primary key (parent__id, " + + (extent == 0 ? "" : "list__index, ") + + "word);" + eoln; + keys.add(pk); + // add the foreign index: + String fkIndex = "create index " + wordTable.fkIndexName + " on " + + wordTable.tableName + " (parent__id" + + (extent == 0 ? "" : ", list__index" ) + ");"; + keys.add(fkIndex); + + // add the fk constraint; + String fk = "alter table " + wordTable.tableName+ eoln + + INDENT + "add constraint " + wordTable.fkName + eoln + + INDENT + "foreign key (parent__id" + + (extent == 0 ? "" : ", list__index" ) + ")" + eoln + + INDENT + "references " + wordTable.parentTableName + eoln + + INDENT + "on delete cascade" + eoln + + INDENT + "on update cascade;"; + keys.add(fk); + wordTablesKeys.put(wordTable.tableName, keys); + } + } + /** * Appends a field to a table DDL statement. Usually one column is added but, if required * (in case of {@code datetime-tz} and case-insensitive {@code character} fields), multiple @@ -1095,6 +1572,60 @@ } /** + * Appends a last field to a table DDL statement. Usually one column is added but, if required + * (in case of {@code datetime-tz} and case-insensitive {@code character} fields), multiple + * column are added. + * + * @param field + * The field to be added. + * @param dialect + * The dialect to be used. + * @param table + * The field's parent table. + * @param sb + * The string builder used for concatenate string data. + * @param eoln + * OS-dependent end of line terminator. + */ + private void addLastFieldImpl(P2JField field, + Dialect dialect, + Table table, + StringBuilder sb, + String eoln) + { + sb.append(INDENT).append(field.getName()).append(" ") + .append(dialect.getSqlMappedType(field.getType().toString(), field.getScale())) + .append(field.isMandatory() ? " not null" : "").append(eoln); + + // special handling for character fields that are index components: + // we handle this in a dialect specific + if (dialect.injectComputedColumns() && + // dialect.needsComputedColumns() && + isIndexComponent(field, table) && + "character".equals(field.getType().toString())) + { + sb.append(INDENT) + .append(dialect.getComputedColumnPrefix(field.isCaseSensitive())) + .append(field.getName()) + .append(" ") + .append(dialect.getSqlMappedType(field.getType().toString(), field.getScale())) + .append(" as ") + .append(dialect.getComputedColumnFormula(field.getName(), !field.isCaseSensitive())) + .append(eoln); + } + + // special handling for datetime-tz: this is handled in a uniform, dialect-independent + // way (Note: this is because in spite of correct time/date arithmetic, the PSQL dialect + // loses the time-zone offset and it cannot be retrieved back) + if ("datetimetz".equals(field.getType().toString())) + { + sb.append(INDENT).append(field.getName()).append(DTZ_OFFSET) + .append(" ").append(dialect.getSqlMappedType("integer", 0)) + .append(field.isMandatory() ? " not null" : "").append(eoln); + } + } + + /** * Checks whether a field is part of an index. The {@code character} case-insensitive fields * index components need special handling. * @@ -1124,20 +1655,43 @@ } /** - * Get the schema name based on database name. + * Construct database object name concatenating parts * - * @param dbName - * The database name. - * - * @return The schema name. + * @param dbName + * The target schema. + * @param dialect + * The {@code Dialect} to be used. + * @param parts + * parts of the name + * @return object name */ - private String getSchemaName(String dbName) + private String createDbObjectName(String dbName, String... parts) { - if (MetadataManager.META_SCHEMA.equals(dbName)) + Set wordSupportName = wordTablesNames.computeIfAbsent(dbName, n -> new HashSet<>()); + String name = Arrays.stream(parts).collect(Collectors.joining(NAME_PARTS_SEPARATOR)); + int excess = name.length() - MAX_OBJECT_NAME_LEN; + if (excess > 0) { - return Configuration.getSchemaConfig().getMetadata().getName(); + int np = 0, len = -1; + for (int i = 0; i < parts.length; i++) + { + if (parts[i].length() > len) + { + np = i; + len = parts[i].length(); + } + } + String longest = parts[np]; + int n = 0; + do + { + String s = String.valueOf(n++); + parts[np] = longest.substring(0, len - excess - s.length()) + s; + name = Arrays.stream(parts).collect(Collectors.joining(NAME_PARTS_SEPARATOR)); + } + while(!wordSupportName.add(name)); } - return dbName; + return name; } } } === added file 'src/com/goldencode/p2j/persist/orm/DMOSignatureHelper.java' --- src/com/goldencode/p2j/persist/orm/DMOSignatureHelper.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/orm/DMOSignatureHelper.java 2020-12-19 00:59:30 +0000 @@ -0,0 +1,841 @@ +/* +** Module : DMOSignatureHelper.java +** Abstract : A collection of methods used for handling DMO signatures. +** +** Copyright (c) 2020, Golden Code Development Corporation. +** +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- +** 001 AIL 20201125 First revision. +** 20201126 Added unique constraint encoding and checking. +** 20201210 Added explicit signature for DMO operations in which the property names matter. +** 20201214 Fixed unique constraint checking. +** 20201216 Added support for loose-copy-mode. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.orm; + +import java.util.*; +import java.util.logging.*; +import java.util.regex.Pattern; + +import com.goldencode.p2j.util.*; + +/** + * Compute a string based signature for a DMO, which contains information regarding the + * type of each field, extents and eventually some restrictions like mandatory or unique. + * In order to have the prefix-code property for each generated string, the following + * standards should be respected: + * - The properties are represented in the order they are provided. + * - A data type representation should be a single upper case character. + * - In case the field has an extent, the length of the extent will follow the data type + * representation as a number (no matter the number of digits) + * - At the end of the representation of one property, there will be a sequence of lower-case + * characters showing which restrictions are imposed on this property (mandatory). The + * order of these is important. + * This means that one property will have the following format: + * UpperCaseType ExtentDigit* LowerCaseConstraint* + * At the end of the property representation, we will have unique constraint representations, + * which is sequence of delimited enumerations of properties id. + */ +public class DMOSignatureHelper +{ + /** + * A map statically initialized with the classic JVM data type single-char representation. + * This can also contain other mappings created at the runtime. + */ + private static final Map, Character> typeMapping; + + /** The character used for representing a mandatory property */ + private static final char CHAR_MANDANTORY = 'm'; + + /** + * The character used as delimiter between unique constraints which are encoded + * as a sequence of property ids + */ + private static final char CHAR_UNIQUE_CONSTRAINT_DELIMITER = '|'; + + /** The character used as delimiter between the property ids in the same group (ex: unique constraint) */ + private static final char CHAR_PROPERTY_ID_DELIMITER = ','; + + /** The character used before appending a property name to the signature. */ + private static final char PROPERTY_NAME_DELIMITER_START = '('; + + /** The character used after appending a property name to the signature. */ + private static final char PROPERTY_NAME_DELIMITER_FINISH = ')'; + + /** A string based delimiter (regex friendly) used for splitting a signature into separate constraints */ + private static final String FORMATTED_CONSTRAINT_DELIMITER; + + /** + * A string based delimiter (regex friendly) used for splitting a propriety id group + * into separate property ids + */ + private static final String FORMATTED_PROPERTY_DELIMITER; + + /** Logger */ + private static final Logger LOG = LogHelper.getLogger(DMOSignatureHelper.class.getName()); + + /** A helper to select a suitable unused character for a specified data type. */ + private static final TypeIdentifierProvider findSuitableCandidate; + + /** A helper to select any unused character for a specified data type. */ + private static final TypeIdentifierProvider findAnyCandidate; + + /** Static initialization of the type mapping */ + static + { + typeMapping = new IdentityHashMap<>(); + + typeMapping.put(character.class, 'C'); + typeMapping.put(decimal.class, 'F'); + typeMapping.put(integer.class, 'I'); + typeMapping.put(int64.class, 'J'); + typeMapping.put(logical.class, 'Z'); + + FORMATTED_CONSTRAINT_DELIMITER = Pattern.quote(String.valueOf(CHAR_UNIQUE_CONSTRAINT_DELIMITER)); + FORMATTED_PROPERTY_DELIMITER = Pattern.quote(String.valueOf(CHAR_PROPERTY_ID_DELIMITER)); + + findSuitableCandidate = (type) -> + { + Set values = new HashSet(typeMapping.values()); + for (char raw : type.getName().toCharArray()) + { + char candidate = Character.toUpperCase(raw); + if (isType(candidate) && !values.contains(candidate)) + { + typeMapping.put(type, candidate); + return candidate; + } + } + return null; + }; + + findAnyCandidate = (type) -> + { + Set values = new HashSet(typeMapping.values()); + for (char candidate = 'A'; candidate <= 'Z'; candidate++) + { + if (!values.contains(candidate)) + { + typeMapping.put(type, candidate); + return candidate; + } + } + return null; + }; + } + + /** + * This builds a signature which represents a DMO. The DMOs which are compatible under certain + * circumstances (structural, by property name etc.) should compatible string signatures. + * + * @param itProvider + * An iterator to the whole collection of properties for the processed DMO. + * @param uniqueConstraints + * A list of multiple properties which together should be unique. + * @param tempTable + * A flag to indicate that that the properties are for a temp-table. + * + * @return A DMO signature representing the a DMO having the specified properties and unique constraints. + */ + public static DmoSignature buildSignature(IteratorProvider itProvider, + List> uniqueConstraints, + boolean tempTable) + { + Map explicitIdToPos = new HashMap<>(); + String quickSignature = buildQuickSignature(itProvider, uniqueConstraints, tempTable); + String explicitSignature = buildExplicitSignature(itProvider, + uniqueConstraints, + tempTable, + explicitIdToPos); + + return new DmoSignature(quickSignature, + explicitSignature, + explicitIdToPos); + } + + /** + * Check if two signatures are compatible from properties name order point of view. This works only + * if both the source and destination have explicit signatures - as the property names are relevant here. + * + * @param srcSignature + * The source signature which should be checked. + * @param dstSignature + * The destination signature which should be checked. + * + * @return {@code true} if the DMO signatures shuffled the property names in the explicit signatures + * in the same order. This means that, if the explicit signature match and this returns + * {@code true}, it means that they have the same order of the properties name/types/extents/ + * constraints etc. Otherwise, they only have the same property names but in different order, + * case in which this returns {@code false}. + */ + public static boolean exactPropertyOrder(DmoSignature srcSignature, DmoSignature dstSignature) + { + if (srcSignature == null || dstSignature == null) + { + return false; + } + + String srcOrder = srcSignature.getIdToPosAsString(); + String dstOrder = dstSignature.getIdToPosAsString(); + + if (srcOrder == null || dstOrder == null) + { + return false; + } + + return srcOrder.equals(dstOrder); + } + + /** + * Check if the source and destination are compatible for a fast buffer-copy. + * + * @param src + * The signature of the source DMO. + * @param dst + * The signature of the destination DMO. + * + * @return {@code true} if the explicit signatures are equal, so they have the same property names + * (not always in the same order). + */ + public static boolean validBufferCopy(DmoSignature src, DmoSignature dst) + { + if (src == null || dst == null) + { + return false; + } + + String srcSignature = src.getExplicitSignature(); + String dstSignature = dst.getExplicitSignature(); + + if (srcSignature == null || dstSignature == null) + { + return false; + } + + return srcSignature.equals(dstSignature); + } + + /** + * Check if the destination signature is less restrictive than the source signature. This checks + * each field for type and extent compatibility; afterwards it checks if all destination constraints + * can be validated by source's constraints. If the signatures are compatible in this way, + * we can allow a fast copy. Note that looseCopy mode will relax the constraints regarding property + * order; all we care about are the unique constraints. + * + * @param src + * The signature of the source DMO. + * @param dst + * The signature of the destination DMO. + * @param looseCopy + * Flag which indicates if the DMO signatures are compatible in loose-copy-mode. + * + * @return {@code true} if the signatures are compatible. + */ + public static boolean validTempTableBulkCopy(DmoSignature src, + DmoSignature dst, + boolean looseCopy) + { + if (src == null || dst == null) + { + return false; + } + + String srcSignature = src.getQuickSignature(); + String dstSignature = dst.getQuickSignature(); + + if (srcSignature == null || dstSignature == null) + { + return false; + } + + if (srcSignature.equals(dstSignature)) + { + return true; + } + + // fall back to an in-detail analysis of the signatures + String[] srcChunks = srcSignature.split(FORMATTED_CONSTRAINT_DELIMITER, 2); + String[] dstChunks = dstSignature.split(FORMATTED_CONSTRAINT_DELIMITER, 2); + + // verify the properties first: split after the unique constraint delimiter and get the first chunk + // in loose-copy-mode, this step is skipped + if (!looseCopy && !propertiesMatch(srcChunks[0], dstChunks[0])) + { + return false; + } + + // verify unique constraints after: take the second chuck and match the unique constraints + // both have unique constraints so resolve through special method + if (srcChunks.length > 1 && dstChunks.length > 1 && !uniqueConstraintsMatch(srcChunks[1], dstChunks[1])) + { + return false; + } + + // the destination has unique constraints, but the source doesn't, so this is not a safe copy + if (srcChunks.length <= 1 && dstChunks.length > 1) + { + return false; + } + + return true; + } + + /** + * This builds a string which structurally represents a DMO. The DMOs which are compatible, + * from strcture's point of view, should have the same signature. This is useful for quickly + * checking compatibility (example: copy-temp-table). + * + * @param it + * An iterator to the whole collection of properties for the processed DMO. + * @param uniqueConstraints + * A list of multiple properties which together should be unique. + * @param tempTable + * A flag to indicate that that the properties are for a temp-table. + * + * @return A string representing the signature for a DMO having the specified properties. + */ + private static String buildQuickSignature(IteratorProvider it, + List> uniqueConstraints, + boolean tempTable) + { + StringBuilder sb = new StringBuilder(); + + if (!buildPropertySignature(it, tempTable, false, null, sb)) + { + return null; + } + + if (!buildUniqueConstraintsSignature(uniqueConstraints, null, sb)) + { + return null; + } + + return sb.toString(); + } + + private static String buildExplicitSignature(IteratorProvider it, + List> uniqueConstraints, + boolean tempTable, + Map idToPos) + { + StringBuilder sb = new StringBuilder(); + + if (!buildPropertySignature(it, tempTable, true, idToPos, sb)) + { + return null; + } + + if (!buildUniqueConstraintsSignature(uniqueConstraints, idToPos, sb)) + { + return null; + } + + return sb.toString(); + } + + /** + * Check if two signatures are compatible in terms of properties data types, extent and field-level + * constraints. + * + * @param src + * The signature of the source DMO. + * @param dst + * The signature of the destination DMO. + * + * @return {@code true} if the signatures are compatible. + */ + private static boolean propertiesMatch(String src, String dst) + { + if (src == null || dst == null) + { + return false; + } + if (src.equals(dst)) + { + return true; + } + + char[] srcChars = src.toCharArray(); + char[] dstChars = dst.toCharArray(); + + int srcLength = srcChars.length; + int dstLength = dstChars.length; + + int i, j; + for (i = 0, j = 0; i < srcLength && j < dstLength;) + { + if (!isType(srcChars[i]) || !isType(dstChars[j])) + { + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, "DMO signatures are malformed; " + + "some tokens are not parsed when validating copy"); + } + return false; + } + + // check type + if (srcChars[i++] != dstChars[j++]) + { + return false; + } + + // check extent + while (i < srcLength && j < dstLength && isDigit(srcChars[i]) && isDigit(dstChars[j])) + { + if (srcChars[i++] != dstChars[j++]) + { + return false; + } + } + + if ((i < srcLength && isDigit(srcChars[i])) || (j < dstLength && isDigit(dstChars[j]))) + { + return false; + } + + // check constraints + while (j < dstLength && isConstraint(dstChars[j])) + { + while (i < srcLength && isConstraint(srcChars[i]) && srcChars[i] != dstChars[j]) + { + i++; + } + + if (i >= srcLength || !isConstraint(srcChars[i])) + { + return false; + } + + // the source has a constraint which matches the current destination constraint + i++; + j++; + } + } + + return i == srcLength && j == dstLength; + } + + /** + * Check if the second set of unique constraints (encoded in the destination signature) can be implied + * by the first set of unique constraints. This will happen if, for each unique constraint in the + * destination, there is a unique constraint in the source which is stronger (having such constraint + * will automatically imply that the destination unique constraint is satisfied). + * + * @param src + * The signature of the source's unique constraints. + * @param dst + * The signature of the destination's unique constraints. + * + * @return {@code true} if the constraints in the destination are satisfied by the unique constraints + * in the source. + */ + private static boolean uniqueConstraintsMatch(String src, String dst) + { + if (src == null || dst == null) + { + return false; + } + + if (src.equals(dst)) + { + return true; + } + + String[] srcConstraints = src.split(FORMATTED_CONSTRAINT_DELIMITER); + String[] dstConstraints = dst.split(FORMATTED_CONSTRAINT_DELIMITER); + int srcLength = srcConstraints.length; + int dstLength = dstConstraints.length; + + for (int j = 0; j < dstLength; j++) + { + boolean foundMatch = false; + for (int i = 0; i < srcLength; i++) + { + if (strongerUniqueConstraint(srcConstraints[i], dstConstraints[i])) + { + foundMatch = true; + break; + } + } + if (!foundMatch) + { + return false; + } + } + + return true; + } + + /** + * Check if having the first unique constraint will automatically determine that the second unique + * constraint will be satisfied. This happens when all fields in the first unique constraint are + * included in the second one. From signature's point of view, this will be resolved by + * finding all encoded ids in the first unique constraint in the second one. Having same signature, + * will automatically mean that the first unique constraint is stronger. + * + * @param firstConstraint + * The first unique constraint signature which is checked. + * @param secondConstraint + * The second unique constraint signature which is checked. + * + * @return {@code true} if the first unique constraint is included in the second, which means + * that is stronger and can imply the second unique constraint. + */ + private static boolean strongerUniqueConstraint(String firstConstraint, String secondConstraint) + { + if (firstConstraint == null || secondConstraint == null) + { + return false; + } + if (firstConstraint.equals(secondConstraint)) + { + return true; + } + + String[] ids1 = firstConstraint.split(FORMATTED_PROPERTY_DELIMITER); + String[] ids2 = secondConstraint.split(FORMATTED_PROPERTY_DELIMITER); + int length1 = ids1.length; + int length2 = ids2.length; + + // we make use of the fact that the property ids are sorted in a strictly ascending way + int i, j; + for (i = 0, j = 0; i < length1 && j < length2; j++) + { + if (ids1[i].equals(ids2[j])) // we found a constraint match for property i + { + i++; + } + } + + // first constraint is stronger, if we could find all its properties in the second constraint + return i == length1; + } + + /** + * This is responsible for encoding the properties and append them to the provided string builder. + * The encoding is a non-delimited sequence of property encodings. Each property encoding is + * formed out of: data type encoding, extent encoding and field-level constraints. + * + * @param itProvider + * An iterator to a collection of properties which should be encoded. + * @param tempTable + * A flag indicating that the signature is created for a temp-table specific DMO. + * @param explicit + * A flag indicating that this property signature is done for an explicit signature. + * This means that the property names should be included, and the property ordered. + * @param idToPos + * A valid map which will be filled with the correspondence between property id and + * their order in the generated signature. This is required only if the explicit + * flag is set on true - as only explicit signatures have a different ordering of the props. + * @param sb + * The string builder onto which the signature should be appended. + * + * @return {@code true} if the properties could be encoded in a signature. + */ + private static boolean buildPropertySignature(IteratorProvider itProvider, + boolean tempTable, + boolean explicit, + Map idToPos, + StringBuilder sb) + { + Map nameToSignature = new LinkedHashMap<>(); + Iterator it = itProvider.provide(); + while (it.hasNext()) + { + StringBuilder sbProp = new StringBuilder(); + Property prop = it.next(); + Class type = (Class) prop._fwdType; + Character ch = typeMapping.get(type); + + if (ch == null) + { + // for this kind of situations, find a suitable representation for this type + ch = findSuitableCandidate.provide(type); + } + + if (ch == null) + { + // can't find a suitable representation, assign a random character + ch = findAnyCandidate.provide(type); + } + + if (ch == null) + { + if (LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, "Couldn't find a character representation for " + type.getName()); + } + return false; + } + + sbProp.append(ch); + + int extent = prop.extent; + if (extent > 0) + { + sbProp.append(extent); + } + + // for explicit signatures, we need to add the name of the property + if (explicit && !processFieldName(prop, tempTable, sbProp)) + { + return false; + } + + if (!processFieldConstraints(prop, tempTable, sbProp)) + { + return false; + } + + nameToSignature.put(prop.legacy, sbProp.toString()); + } + + if (!explicit) + { + // this is a quick signature: append the properties in the default order + for (String signature : nameToSignature.values()) + { + sb.append(signature); + } + return true; + } + + // this is an explicit signature: append the properties sorted by the name + Map name2Pos = new HashMap<>(); + int idx = 1; + for (Map.Entry entry : new TreeMap<>(nameToSignature).entrySet()) + { + String name = entry.getKey(); + String signature = entry.getValue(); + + sb.append(signature); + name2Pos.put(name, idx++); + } + + // create a positioning mapping as the fields are not in the same order in the + // signature as in a record + it = itProvider.provide(); + while (it.hasNext()) + { + Property prop = it.next(); + idToPos.put(prop.id, name2Pos.get(prop.legacy)); + } + + return true; + } + + /** + * This is responsible for encoding the unique constraints and append them to the provided string builder. + * The encoding is a lexicographically sorted sequence of unique constraint signatures, delimited by a + * defined character. Each unique constraint signature is a sorted sequence of property ids, delimited + * by a fixed character. + * + * @param uniqueConstraints + * A list of unique constrains (each being a list of properties). + * @param idToPos + * A mapping from the property ids to their position in the signature. This should be + * {@code null} for quick signatures (as their ids match the position}, which this should be + * not {@code null} null in explicit signatures, where the order doesn't always match. + * @param sb + * The string builder onto which the signature should be appended. + * + * @return {@code true} if the unique constraints could be converted to a signature + */ + private static boolean buildUniqueConstraintsSignature(List> uniqueConstraints, + Map idToPos, + StringBuilder sb) + { + if (uniqueConstraints == null || uniqueConstraints.isEmpty()) + { + return true; + } + + List constraintSignatures = new ArrayList<>(); + + for (List props : uniqueConstraints) + { + if (props == null || props.isEmpty()) + { + continue; + } + + StringBuilder signature = new StringBuilder(); + List ids = new ArrayList<>(props.size()); + + props.forEach((prop) -> ids.add(idToPos == null ? prop.id : idToPos.get(prop.id))); + + // the ids should be sorted in order to do exact matches more easily (string matching) + Collections.sort(ids); + for (int i = 0; i < ids.size() - 1; i++) + { + signature.append(ids.get(i)).append(CHAR_PROPERTY_ID_DELIMITER); + } + signature.append(ids.get(ids.size() - 1)); + + constraintSignatures.add(signature.toString()); + } + + // the constraints should be sorted in order to do exact matches more easily (string matching) + Collections.sort(constraintSignatures); + + for (int i = 0; i < constraintSignatures.size(); i++) + { + sb.append(CHAR_UNIQUE_CONSTRAINT_DELIMITER).append(constraintSignatures.get(i)); + } + + return true; + } + + /** + * Check if the provided character is suitable for data type encoding in the signature. + * + * @param ch + * The character which is checked. + * + * @return {@code true} if the character is suitable. + */ + private static boolean isType(char ch) + { + return 'A' <= ch && ch <= 'Z'; + } + + /** + * Check if the provided character is a base-10 digit. + * + * @param ch + * The character which is checked. + * + * @return {@code true} if the character is a digit. + */ + private static boolean isDigit(char ch) + { + return '0' <= ch && ch <= '9'; + } + + /** + * Check if the provided character is suitable for constraint encoding in the signature. + * + * @param ch + * The character which is checked. + * + * @return {@code true} if the character is suitable. + */ + private static boolean isConstraint(char ch) + { + return 'a' <= ch && ch <= 'z'; + } + + /** + * Helper which sets restrictions like mandatory or unique to a specified field. + * + * @param prop + * The property for which the restriction analysis is done. + * @param tempTable + * Flag which indicates that the property is part of a temporary table. + * @param sb + * The string builder which should be completed with the found restrictions. + * + * @return {@code true} if we could process the constraints of the property + */ + private static boolean processFieldConstraints(Property prop, + boolean tempTable, + StringBuilder sb) + { + if (!tempTable && prop.mandatory) + { + sb.append(CHAR_MANDANTORY); + } + + return true; + } + + private static boolean processFieldName(Property prop, + boolean tempTable, + StringBuilder sb) + { + String name = prop.legacy; + if (name.indexOf(PROPERTY_NAME_DELIMITER_START) != -1 || + name.indexOf(PROPERTY_NAME_DELIMITER_FINISH) != -1) + { + return false; + } + + sb.append(PROPERTY_NAME_DELIMITER_START); + sb.append(name); + sb.append(PROPERTY_NAME_DELIMITER_FINISH); + return true; + } + + /** + * Helper for anonymous functions used in identifying a suitable character for a specified data type. + */ + interface TypeIdentifierProvider + { + /** Single provider method for a character based on a base data type class. */ + Character provide(Class type); + } + + /** + * Helper for anonymous functions used in identifying a suitable character for a specified data type. + */ + interface IteratorProvider + { + /** Single provider method for an iterator */ + Iterator provide(); + } +} === modified file 'src/com/goldencode/p2j/persist/orm/DmoClass.java' --- src/com/goldencode/p2j/persist/orm/DmoClass.java 2020-09-20 08:07:24 +0000 +++ src/com/goldencode/p2j/persist/orm/DmoClass.java 2020-10-14 21:06:19 +0000 @@ -180,7 +180,7 @@ String wide = (String) bdtInfo[i + 2]; BaseDataAccess bda = new BaseDataAccess(); - bda.logical = type.isAssignableFrom(logical.class); + bda.logical = (type == logical.class); String upperNarrow = StringHelper.changeCase(simpleNarrow, true); bda.getter = "_" + (bda.logical ? "is" : "get") + upperNarrow; === modified file 'src/com/goldencode/p2j/persist/orm/DmoMeta.java' --- src/com/goldencode/p2j/persist/orm/DmoMeta.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/orm/DmoMeta.java 2021-01-29 19:50:19 +0000 @@ -14,6 +14,14 @@ ** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201012 Moved here delegated methods from manager. +** AIL 20201125 Added DMO signature computation in constructor. +** 20201126 Added unique constraints to DMO signature. +** 20201203 Added helper methods. +** 20201210 Changes signature from string to specialized class. +** OM 20201120 Added NO-UNDO attribute support. +** OM 20201202 Added getPrimaryIndex() method. +** IAS 20201210 Added support for word tables. */ /* @@ -71,6 +79,7 @@ package com.goldencode.p2j.persist.orm; +import com.goldencode.p2j.convert.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.annotation.*; import com.goldencode.p2j.persist.annotation.Index; @@ -84,6 +93,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.logging.*; +import java.util.stream.*; /** * An immutable structure that contains the annotation information on a table and its fields. @@ -107,6 +117,9 @@ /** The set of annotations for each declared field, mapped by the FQL property name. */ protected Map fields = new LinkedHashMap<>(); + /** The set of annotations for each declared field, mapped by its legacy name. */ + protected Map fieldsByLegacy = null; + /** The list of indexes of this table. */ protected List indexes = new ArrayList<>(); @@ -140,6 +153,9 @@ /** The cached list of sets of properties participating in UNIQUES indexes. */ private List> uniqueConstraints = null; + /** The cached list of lists of whole property instances participating in UNIQUES indexes. */ + private List> uniqueProps = null; + /** The cached list of mandatory fields. */ private List mandatoryFields = null; @@ -197,6 +213,12 @@ /** Caches the {@code namespacePrefix} attribute from {@link Table} annotation. */ public final String namespacePrefix; + /** A signature containing information about the DMO structure and field constraints. */ + private final DmoSignature signature; + + /** Caches the {@code NO-UNDO} attribute from {@link Table} annotation (TEMP-TABLE specific). */ + public final boolean noUndo; + /** The set of properties and their metadata, indexed by their in-memory positions. */ private Pair[] properties = null; @@ -254,6 +276,7 @@ this.xmlNodeName = table.xmlNodeName(); this.namespaceUri = table.namespaceUri(); this.namespacePrefix = table.namespacePrefix(); + this.noUndo = table.noUndo(); this.iface = iface; this.dmoImplInterface = dmoImplInterface; @@ -309,6 +332,14 @@ fields.put(property.name, property); } + // build a collection of lists containing properties part of the same unique constraint + // this will be used when encoding unique constraints in the signature + List> uniqueProps = getUniqueConstraintsAsProps(); + + // build a String signature suitable for identifying if two DMOs are compatible in certain + // operations (for example copy-temp-table). + signature = DMOSignatureHelper.buildSignature(() -> getFields(false), uniqueProps, tempTable); + // the eventual relations cannot be created at this moment, they will be processed at a // later time, when the respective DMO interfaces will be loaded. } @@ -466,6 +497,49 @@ } /** + * Looks up a {@code Property} by its legacy field name.

+ * Note:
+ * This method is rather slow. Do not use it unless you have no other solution.
+ * At the moment when this method was added, it is only needed from within {@code serial} package. This + * is also the reason it is {@code public}. + * + * @param legacyField + * The name of the legacy field. + * + * @return The {@code Property} whose legacy name was passed as parameter. + */ + public Property byLegacyName(String legacyField) + { + // be sure the name is normalized + legacyField = legacyField.toLowerCase().intern(); + + if (fieldsByLegacy == null) + { + Property ret = null; + IdentityHashMap tmpMap = new IdentityHashMap<>(); + Set> entries = fields.entrySet(); + for (Map.Entry entry : entries) + { + Property prop = entry.getValue(); + if (prop.id > 0) // skipping reserved properties + { + String key = prop.legacy.toLowerCase().intern(); + tmpMap.put(key, prop); + if (legacyField == key) // this is OK, they are both interned + { + ret = prop; + } + } + } + + this.fieldsByLegacy = Collections.unmodifiableMap(tmpMap); + return ret; + } + + return fieldsByLegacy.get(legacyField); + } + + /** * Obtain the list of properties for this DMO. * * @param extra @@ -502,6 +576,105 @@ } /** + * Get the fields of a table. + * + * @return collection of the fields of the target table. + */ + public List getExistingFields() + { + if (existingFields == null) + { + Iterator it = getFields(true); + + ArrayList ret = new ArrayList<>(fields.size()); + while (it.hasNext()) + { + Property p = it.next(); + if (p.id < 0) + { + // skip ReservedProperties + continue; + } + + Class bdtType = p._fwdType; + Object init = null; + if (BaseDataType.class.isAssignableFrom(bdtType)) + { + try + { + Constructor ctor = bdtType.getConstructor(String.class); + init = ctor.newInstance(p.initial); + } + catch (NoSuchMethodException | + IllegalAccessException | + InstantiationException | + InvocationTargetException e) + { + if (log.isLoggable(Level.SEVERE)) + { + log.log(Level.SEVERE, + "Failed to instantiate default value for " + p.name + " of " + iface); + } + } + } + ret.add(new P2JField(p.name, + ParmType.fromClass(bdtType), + p.extent, + p.format, + (BaseDataType) init, + p.label, + p.columnLabel, + p.caseSensitive, + p.codePage, + p.help, + p.serializeHidden, + p.serializeName, + p.xmlDataType, + p.xmlNodeName, + p.xmlNodeType, + p.mandatory, + p.scale)); + ret.trimToSize(); + existingFields = Collections.unmodifiableList(ret); + } + } + return existingFields; + } + + /** + * Determine whether the given DMO property represents the leading component of any index on this table. + * + * @param property + * DMO property to be tested. + * + * @return {@code true} if {@code property} is a leading index component; else {@code false}. + */ + public boolean isLeadingIndexComponent(String property) + { + Iterator ixdIt = getDatabaseIndexes(); + + if (ixdIt == null) + { + return false; + } + + while (ixdIt.hasNext()) + { + Iterator propsIter = ixdIt.next().components(); + if (propsIter.hasNext()) + { + P2JIndexComponent leadingProp = propsIter.next(); + if (property.equals(leadingProp.getPropertyName())) + { + return true; + } + } + } + + return false; + } + + /** * Return the number of indices for this table (declared in DMO annotations). Depending on the parameter * the number of unique or non-unique indices will be returned. * @@ -515,8 +688,47 @@ { return unique ? recordMeta.getUniqueIndexNames().length : recordMeta.getNonuniqueIndexNames().length; } + /** + * Get word tables' names map by the field name + * + * @return Word tables' names map by the field name. + */ + public Map getWordTables() + { + Map map = new HashMap<>(); + for(Index idx: indexes) + { + if (!idx.word()) + { + continue; + } + String fieldName = idx.components()[0].name(); + String wordtablename = idx.wordtablename(); + String extentTableName = null; + Property property = fields.get(fieldName); + if (property != null) + { + int extent = property.extent; + extentTableName = extent == 0 ? null: sqlTable + "__" + extent; + fieldName = property.column; + } + map.put(fieldName, new WordIndexData(wordtablename, extentTableName, + property.caseSensitive)); + } + return map; + } /** + * Get word tables' names map by the index name + * + * @return Word tables' names map by the index name. + */ + public Map getWordTablesByIndexName() + { + return indexes.stream().filter(Index::word). + collect(Collectors.toMap(idx -> idx.name(), Index::wordtablename)); + } + /** * Get an iterator on all indexes of the DMO. The resulting iterator returns {@code P2JIndex} * objects, each of which defines the columns participating in an index. * @@ -530,7 +742,8 @@ for (Index index : indexes) { P2JIndex p2jIndex = new P2JIndex( - sqlTable, index.name(), index.unique(), index.primary(), index.word()); + sqlTable, index.name(), index.unique(), index.primary(), + index.word(), index.wordtablename()); for (IndexComponent comp : index.components()) { String compName = comp.name(); @@ -551,6 +764,38 @@ } /** + * Get the primary index. If a PRIMARY index is found it is returned. If no index was declared PRIMARY and + * {@code implicit == false} the method returns {@code false}. If {@code implicit == true}, the first + * declared index is returned. If the table does not define any fields, {@code null} is returned. + * + * @param implicit + * If {@code true}, if there is no explicit primary index, the first index is returned as being + * the implicit primary index. Otherwise, in the event there is no primary index declared, + * the method returns {@code null}. + * + * @return The PRIMARY index, the primary IMPLICIT index or {@code null} as described above. + */ + public P2JIndex getPrimaryIndex(boolean implicit) + { + Iterator it = getDatabaseIndexes(); + if (!it.hasNext()) + { + return null; // no indexes defined, whatsoever + } + + while (it.hasNext()) + { + P2JIndex index = it.next(); + if (index.isPrimary()) + { + return index; + } + } + + return implicit ? this.databaseIndexes.get(0) : null; + } + + /** * Get the list of mandatory fields. * * @return The {@link #mandatoryFields}. @@ -653,6 +898,33 @@ } /** + * Obtain the lists of unique constraints for each DMO. That is, the list of index components + * for each unique index for this DMO. Uses lazy initialization, the list for each DMO is + * computed at first access. + * + * @return list of lists of whole property instances participating in UNIQUES indexes. + */ + public List> getUniqueConstraintsAsProps() + { + if (uniqueProps == null) + { + uniqueProps = new ArrayList<>(); + List> uniqueIdxs = getUniqueConstraints(); + for (Set idx : uniqueIdxs) + { + List props = new ArrayList<>(); + for (String prop : idx) + { + props.add(fields.get(prop)); + } + uniqueProps.add(props); + } + } + + return uniqueProps; + } + + /** * Get the {@code Property} that correspond to the k-th entry in {@code data} table of the class * implementing this DMO. * @@ -984,6 +1256,16 @@ } /** + * Get the structure-based signature for this DMO. + * + * @return The signature of this DMO. + */ + public DmoSignature getSignature() + { + return signature; + } + + /** * Obtain a short description of this object, usually for debugging. * * @return a short description of this object. @@ -993,4 +1275,123 @@ { return "DmoMeta{" + iface.getName() + " SQL:" + sqlTable + " 4GL:" + legacyTable + '}'; } + + /** + * Constructs and returns a string containing the P4GL schema definition of the table. The result is not + * cached so this method is to be used only for debug. + * + * @return a string containing the P4GL legacy schema definition of the table. + */ + public String getTableDefinition() + { + StringBuilder sb = new StringBuilder("DEFINE "); + sb.append(tempTable ? "TEMP-TABLE " : "TABLE ").append(legacyTable); + if (noUndo) + { + sb.append(" NO-UNDO"); + } + if (!namespaceUri.isEmpty()) + { + sb.append("\n NAMESPACE-URI ").append(namespaceUri); + } + if (!namespacePrefix.isEmpty()) + { + sb.append("\n NAMESPACE-PREFIX ").append(namespacePrefix); + } + if (!xmlNodeName.isEmpty()) + { + sb.append("\n XML-NODE-NAME ").append(xmlNodeName); + } + if (!serializeName.isEmpty()) + { + sb.append("\n SERIALIZE-NAME ").append(namespaceUri); + } + if (!beforeTable.isEmpty()) + { + sb.append("\n BEFORE-TABLE ").append(beforeTable); + } + for (Map.Entry entry : fields.entrySet()) + { + Property p = entry.getValue(); + if (p.id < 0) + { + continue; // this is a hidden/reserved property, skip it + } + sb.append("\n FIELD ").append(p.legacy).append(" AS ").append(p._ablType.toUpperCase()); + if (p.caseSensitive) + { + sb.append(" CASE-SENSITIVE"); + } + if (p.extent > 0) + { + sb.append(" EXTENT ").append(p.extent); + } + if (p.scale != 0) + { + sb.append(" DECIMALS ").append(p.scale); + } + if (p.initialNull) + { + sb.append(" INITIAL ?"); + } + else if (!p.initial.isEmpty()) + { + sb.append(" INITIAL ").append(p.initial); + } + } + + for (Index index : indexes) + { + sb.append("\n INDEX ").append(index.legacy()).append(" AS "); + if (index.primary()) + { + sb.append("PRIMARY "); + } + if (index.unique()) + { + sb.append("UNIQUE "); + } + if (index.word()) + { + sb.append("WORD-INDEX "); + } + for (IndexComponent comp : index.components()) + { + sb.append(comp.legacy()).append(comp.descending() ? " DESC " : " "); + } + } + return sb.toString(); + } + /** + * Word index data. + */ + public static class WordIndexData + { + /** word table name */ + public final String wordTableName; + + /** extent table name */ + public final String extentTableName; + + /** Flag indicating that field is case sensitive */ + public final boolean caseSensitive; + + /** + * Constructor + * + * @param wordTableName + * Word table name + * @param extentTableName + * Extent table name + * @param caseSensitive + * Flag indicating that field is case sensitive + */ + public WordIndexData(String wordTableName, String extentTableName, boolean caseSensitive) + { + this.wordTableName = wordTableName; + this.extentTableName = extentTableName; + this.caseSensitive = caseSensitive; + } + + } } === modified file 'src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java' --- src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java 2020-12-24 21:27:01 +0000 @@ -12,6 +12,8 @@ ** initialized. ** OM 20200924 Index components carry multiple information to avoid map lookups for them. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** OM 20201012 Moved delegated method to DmoMeta. Deprecated / dropped unused methods. +** IAS 20201224 Added getDmoInfo(String dmoName) method */ /* @@ -69,12 +71,10 @@ package com.goldencode.p2j.persist.orm; -import com.goldencode.p2j.convert.*; import com.goldencode.p2j.directory.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.annotation.*; import com.goldencode.p2j.util.*; -import java.lang.reflect.*; import java.util.*; import java.util.concurrent.*; import java.util.logging.*; @@ -254,69 +254,16 @@ * * @throws IllegalArgumentException * if any of its interfaces is recognized as a valid DMO interface. + * + * @deprecated */ + @Deprecated public static Class getDMOInterface(Class dmoClass) { return getDmoInfo(dmoClass).getAnnotatedInterface(); } /** - * Get the implementation class mapped to the specified interface. - * - * @param iface - * Interface for which an implementing class name is being looked up. - * - * @return DMO class which implements interface {@code iface}, or {@code null} if no such - * mapping is found. - * - * @throws NullPointerException - * if {@code dmoIface} is null. - * @throws IllegalArgumentException - * if {@code iface} parameter is not recognized. - */ - public static Class getImplementingClass(Class iface) - { - if (iface == null) - { - throw new NullPointerException("Parameter of registerDmo() cannot be null."); - } - - DmoMeta dmoMeta = registry.get(iface.getName()); - return dmoMeta == null ? null : dmoMeta.implClass; - } - - /** - * Determine whether the property defined by the given parameters handles - * its data in a case-sensitive manner. - * - * @param iface - * DMO interface. - * @param propertyName - * Name of property. - * - * @return {@code true} if the property is case-sensitive; else {@code false}. - * - * @throws IllegalArgumentException - * if the class passed in is not implementing DMO interface or its interface was not - * registered beforehand or the property is invalid. - * @throws IllegalStateException - * if P2J_HOME is not defined; if there is any error parsing the DMO index document. - * - * @deprecated try to use DmoProperty directly - */ - @Deprecated - public static boolean isCaseSensitive(Class iface, String propertyName) - { - Property property = getDmoInfo(iface).fields.get(propertyName); - if (property == null) - { - throw new IllegalArgumentException( - "The " + iface.getName() + " does not expose a '" + propertyName + "' property."); - } - return property.caseSensitive; - } - - /** * Searches for a {@code DmoMeta} for a specific alias in the list of all known aliases. * * @param dmoName @@ -365,15 +312,34 @@ */ public static DmoMeta getDmoInfo(Class dmo) { - DmoMeta dmoMeta = registry.get(dmo.getName()); + return getDmoInfo(dmo.getName()); + } + + /** + * Searches for a {@code DmoMeta} for a specific alias in the list of all known aliases. The + * method does the best effort to find the right information, regardless the {@code dmo} + * parameter is a DMO interface or an DMO implementation class. + * + * @param dmoName + * The name of the DMO class to be located. + * + * @return The {@code DmoMeta} structure corresponding to DMO class + * + * @throws IllegalArgumentException + * if the class passed in is not implementing DMO interface or its interface was not + * registered beforehand. + */ + public static DmoMeta getDmoInfo(String dmoName) + { + DmoMeta dmoMeta = registry.get(dmoName); if (dmoMeta == null) { - throw new IllegalArgumentException("The " + dmo.getName() + " was not registered yet."); + throw new IllegalArgumentException("The " + dmoName + " was not registered yet."); } return dmoMeta; } - + /** * Obtain the {@code DmoMeta} of a DMO/table based on its unique id. * @@ -398,69 +364,13 @@ * DMO class. * * @return collection of the fields of the target table. + * + * @deprecated use {@link DmoMeta#getExistingFields()} instead. */ + @Deprecated public static List getExistingFields(Class dmoClass) { - Class dmoIface = searchDMOInterface(dmoClass); - DmoMeta meta = DmoMetadataManager.getDmoInfo(dmoIface); - - if (meta.existingFields == null) - { - Iterator it = meta.getFields(true); - - ArrayList ret = new ArrayList<>(meta.fields.size()); - while (it.hasNext()) - { - Property p = it.next(); - if (p.id < 0) - { - // skip ReservedProperties - continue; - } - - Class bdtType = p._fwdType; - Object init = null; - if (BaseDataType.class.isAssignableFrom(bdtType)) - { - try - { - Constructor ctor = bdtType.getConstructor(String.class); - init = ctor.newInstance(p.initial); - } - catch (NoSuchMethodException | - IllegalAccessException | - InstantiationException | - InvocationTargetException e) - { - if (LOG.isLoggable(Level.SEVERE)) - { - LOG.log(Level.SEVERE, - "Failed to instantiate default value for " + p.name + " of " + dmoIface); - } - } - } - ret.add(new P2JField(p.name, - ParmType.fromClass(bdtType), - p.extent, - p.format, - (BaseDataType) init, - p.label, - p.columnLabel, - p.caseSensitive, - p.codePage, - p.help, - p.serializeHidden, - p.serializeName, - p.xmlDataType, - p.xmlNodeName, - p.xmlNodeType, - p.mandatory, - p.scale)); - ret.trimToSize(); - meta.existingFields = Collections.unmodifiableList(ret); - } - } - return meta.existingFields; + return DmoMetadataManager.getDmoInfo(dmoClass).getExistingFields(); } /** @@ -507,24 +417,6 @@ } /** - * Get an iterator on all unique constraints for the DMO specified by the given (schema ? and) - * interface. The resulting iterator returns sets of property names, each of which defines the - * properties participating in a unique constraint. - * - * @param iface - * DMO interface. - * - * @return Iterator on unique constraint property name sets. - * - * @throws IllegalArgumentException - * if either the {@code schema} or {@code iface} parameters are not recognized. - */ - public static Iterator> uniqueConstraints(Class iface) - { - return getDmoInfo(iface).getUniqueConstraints().iterator(); - } - - /** * Determine whether the given DMO property represents the leading component of any index * on the table represented by the given buffer. * @@ -534,30 +426,13 @@ * DMO property to be tested. * * @return {@code true} if {@code property} is a leading index component; else {@code false}. + * + * @deprecated use {@link DmoMeta#isLeadingIndexComponent(String)}. */ + @Deprecated public static boolean isLeadingIndexComponent(Class dmo, String property) { - Iterator ixdIt = getDmoInfo(dmo).getDatabaseIndexes(); - - if (ixdIt == null) - { - return false; - } - - while (ixdIt.hasNext()) - { - Iterator propsIter = ixdIt.next().components(); - if (propsIter.hasNext()) - { - P2JIndexComponent leadingProp = propsIter.next(); - if (property.equals(leadingProp.getPropertyName())) - { - return true; - } - } - } - - return false; + return getDmoInfo(dmo).isLeadingIndexComponent(property); } /** @@ -638,8 +513,7 @@ } /** - * Searches and returns the super-interface {@code DataModelObject} that has the {@code Table} - * annotation. + * Searches and returns the super-interface {@code DataModelObject} that has the {@code Table} annotation. * * @param dmoIface * The interface to be analysed. @@ -647,7 +521,7 @@ * @return the super-interface {@code DataModelObject} that has the {@code Table} annotation * or {@code null} if no such interface can be found. */ - public static Class getAnnotatedInterface( + private static Class getAnnotatedInterface( Class dmoIface) { if (dmoIface.getAnnotation(Table.class) != null) @@ -658,8 +532,7 @@ Class[] interfaces = dmoIface.getInterfaces(); for (Class iface : interfaces) { - if (DataModelObject.class.isAssignableFrom(iface) && - iface.getAnnotation(Table.class) != null) + if (DataModelObject.class.isAssignableFrom(iface) && iface.getAnnotation(Table.class) != null) { return (Class) iface; } @@ -670,50 +543,6 @@ } /** - * Searches for the DMO interface associated with the given DMO implementation class. - * - * @param dmoClass - * DMO implementation class. - * - * @return DMO interface. - * - * @throws IllegalArgumentException - * if any of its interfaces is recognized as a valid DMO interface. - */ - private static Class searchDMOInterface(Class dmoClass) - { - // validation first: - if (!DataModelObject.class.isAssignableFrom(dmoClass)) - { - throw new IllegalArgumentException("Not implementing DataModelObject."); - } - - List> interfaces = new LinkedList<>(Arrays.asList(dmoClass.getInterfaces())); - while (!interfaces.isEmpty()) - { - Class anInterface = interfaces.get(0); - interfaces.remove(0); - - if (DataModelObject.class.isAssignableFrom(anInterface)) - { - if (anInterface.getAnnotation(Table.class) != null) - { - return (Class) anInterface; - } - else - { - // recursive call to detect next level interfaces in DFS order - interfaces.addAll(0, Arrays.asList(anInterface.getInterfaces())); - } - } - } - - // this is funny. We should not have got here. Unless the DMO is not annotated with [Table] - throw new IllegalArgumentException( - "Failed to find the annotated DMO interface for " + dmoClass.getName() + "."); - } - - /** * Extracts the schema from a DMO class name. It is computed as a very last package path part from the * class/interface full name. * === added file 'src/com/goldencode/p2j/persist/orm/DmoSignature.java' --- src/com/goldencode/p2j/persist/orm/DmoSignature.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/orm/DmoSignature.java 2020-12-14 18:27:21 +0000 @@ -0,0 +1,158 @@ +/* +** Module : DmoSignature.java +** Abstract : A way to structurally identify a DMO through string representations. +** +** Copyright (c) 2020, Golden Code Development Corporation. +** +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- +** 001 AIL 20201214 First revision. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ +package com.goldencode.p2j.persist.orm; + +import java.util.*; + +/** + * This structure keeps together representative strings for one DMO. This is useful when we need to check + * fast if two DMOs are compatible (if they signatures are compatible - eventually equal). * + */ +public class DmoSignature +{ + /** Delimiter between the original property id and its position in the explicit signature */ + private static final String KEY_VALUE_DELIMITER = "->"; + + /** Delimiter between multiple pairs of mappings between property id and signature position */ + private static final String ENTRY_DELIMITER = ","; + + /** A string which has information only about property order, types and constraints */ + private final String quickSignature; + + /** A comprehensive string which has information regarding property names, types and constraints */ + private final String explicitSignature; + + /** + * A mapping useful only when an explicit signature exists. + * This is used to store a mapping between the original property id (order) + * and their new positions in the explicit signature. + */ + private final Map explicitIdToPos; + + /** A signature for the {@code explicitIdToPos} mapping, which can fasten the signature matching*/ + private final String idToPosString; + + /** + * Basic constructor. + * + * @param quickSignature + * A quick signature for the target DMO. + * @param explicitSignature + * An explicit signature for the target DMO. + * @param explicitIdToPos + * A mapping between the original property ids and their positions in the explicit signature + */ + public DmoSignature(String quickSignature, + String explicitSignature, + Map explicitIdToPos) + { + this.quickSignature = quickSignature; + this.explicitSignature = explicitSignature; + this.explicitIdToPos = explicitIdToPos; + + if (explicitIdToPos != null) + { + List signatures = new ArrayList<>(); + for (Map.Entry entry : explicitIdToPos.entrySet()) + { + signatures.add(entry.getKey() + KEY_VALUE_DELIMITER + entry.getValue()); + } + idToPosString = String.join(ENTRY_DELIMITER, signatures); + } + else + { + idToPosString = null; + } + } + + /** + * Getter for the quick signature. + * + * @return The quick signature. + */ + public String getQuickSignature() + { + return quickSignature; + } + + /** + * Getter for the explicit signature. + * + * @return The explicit signature. + */ + public String getExplicitSignature() + { + return explicitSignature; + } + + /** + * Getter for the id to position mapping signature. + * + * @return The signature of the if to position mapping. + */ + public String getIdToPosAsString() + { + return idToPosString; + } + +} === modified file 'src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java' --- src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java 2020-10-05 18:05:51 +0000 +++ src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java 2021-01-29 19:50:19 +0000 @@ -1,14 +1,16 @@ /* - ** Module : FqlToSqlConverter.java - ** Abstract : Handles conversion of FQL statements to SQL. - ** +** Module : FqlToSqlConverter.java +** Abstract : Handles conversion of FQL statements to SQL. +** ** Copyright (c) 2019-2020, Golden Code Development Corporation. - ** +** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 OM 20190915 First revision. Converts simple FQL queries into SQL. -** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** 002 OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** CA 20201003 Replaced Guava identity HashSet with Collections.newSetFromMap(IdentityHashMap). ** CA 20201005 Do not emit NULLS option for _multiplex and PK columns, in ORDER BY. +** OM 20201120 Avoid NPE in getScopedDmoInfo(). Changes error message to a more explicit one. +** IAS 20201126 Re-write SQL statement with CONTAINS to use words' table */ /* @@ -66,18 +68,28 @@ package com.goldencode.p2j.persist.orm; -import antlr.*; +import static com.goldencode.p2j.persist.orm.FQLParserTokenTypes.*; + +import java.io.*; +import java.util.*; +import java.util.function.*; +import java.util.logging.*; +import java.util.stream.*; + +import org.apache.commons.lang3.tuple.*; + import com.goldencode.ast.*; +import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.dialect.*; +import com.goldencode.p2j.persist.orm.Query.*; +import com.goldencode.p2j.persist.meta.*; import com.goldencode.p2j.util.*; -import org.apache.commons.lang3.tuple.*; -import java.io.*; -import java.util.*; -import java.util.function.*; -import java.util.logging.*; +import com.goldencode.p2j.util.ErrorManager; +import com.goldencode.p2j.util.LogicalExpressionConverter.*; +import com.goldencode.p2j.persist.orm.DmoMeta.*; -import static com.goldencode.p2j.persist.orm.FQLParserTokenTypes.*; +import antlr.*; /** * This class handles conversion of FQL statements into SQL using a specified @@ -89,6 +101,9 @@ /** Logger */ private static final Logger LOG = LogHelper.getLogger(FqlToSqlConverter.class.getName()); + /** Name of the primary key column. */ + private static final String PK = Configuration.getSchemaConfig().getPrimaryKeyName(); + /** Constant for a virtual alias when none is specified (used in case of single table statements). */ private static final String NO_ALIAS = ""; @@ -128,6 +143,7 @@ */ private final boolean generateUniqueSqlColumnNames; + /** The maximum numbers of results in a query. Used for paging results. */ private int maxResults = -1; @@ -156,6 +172,12 @@ /** Flag identifying to alias will be emitted for properties. Required by the UPDATE statement. */ private boolean forceNoAlias = false; + /** Query params */ + private QueryParams queryParams; + + /** Flags the a query was re-written during conversion to SQL */ + private boolean queryWasRewritten = false; + /** * The constructor is private. To create an instance of this class, use the {@code getInstance} * builder method. @@ -164,14 +186,20 @@ * The SQL dialect to be used when generating SQL statements. * @param schema * The schema of the database. This is used to unambiguate short-names entities. + * @param queryParams + * queryParameters; * @param useSqlTableAlias * When {@code true}, the SQL names will be used for table aliases, otherwise the * FQL named received in FQL string will be kept. */ - private FqlToSqlConverter(Dialect dialect, String schema, boolean useSqlTableAlias) + private FqlToSqlConverter(Dialect dialect, + String schema, + QueryParams queryParams, + boolean useSqlTableAlias) { this.dialect = dialect; this.schema = schema; + this.queryParams = queryParams; this.useSqlTableAlias = useSqlTableAlias; this.generateUniqueSqlColumnNames = true; } @@ -183,13 +211,41 @@ * The SQL dialect to be used when generating SQL statements. * @param schema * The schema of the database. This is used to unambiguate short-names entities. + * @return an instance of this class */ public static FqlToSqlConverter getInstance(Dialect dialect, String schema) { - return new FqlToSqlConverter(dialect, schema, true); + return getInstance(dialect, schema, new QueryParams()); } /** + * Builder method for create an instance of this class. + * + * @param dialect + * The SQL dialect to be used when generating SQL statements. + * @param schema + * The schema of the database. This is used to unambiguate short-names entities. + * @param queryParams + * queryParameters; + * @return an instance of this class + */ + public static FqlToSqlConverter getInstance(Dialect dialect, String schema, + QueryParams queryParams) + { + return new FqlToSqlConverter(dialect, schema, queryParams, true); + } + + /** + * Check if the query was re-written + * + * @return true if the query was re-written + */ + public boolean isQueryWasRewritten() + { + return queryWasRewritten; + } + + /** * Converts a {@code FQL} statement into its SQL representation. * * @param fql @@ -1153,10 +1209,10 @@ * Note: only a one-argument functions are supported (like {@code upper} and {@code rtrim} used * for character fields). If more complex expressions are needed the more generic * {@code generateExpression()} should be used instead. - * + * * @param node * The root node for this expression. - * + * * @return true if this is the '_multiplex' or PK column. */ private boolean generateOrderByComponent(FQLAst node) @@ -1257,11 +1313,36 @@ * @param qualified * {@code true} if property is qualified by an alias; {@code false} if {@code node} * represents an unqualified property name. - * + * * @return true if this is the '_multiplex' or PK column. */ private boolean generateProperty(FQLAst node, boolean forceCsPrefix, boolean qualified) { + return generateProperty(node, forceCsPrefix, qualified, null); + } + + /** + * Converts a FQL property to a SQL field name. The output is directly appended to + * {@code sb}. + * + * @param node + * The {@code alias} or {@code property} node. If the former, this represents the table this + * property belongs to. Normally it should have a single child, with the name of the {@code + * PROPERTY}. If the latter, this represents an unqualified property name (and a single-table + * statement is assumed). + * @param forceCsPrefix + * If {@code true}, the dialect specific case-sensitive prefix will be injected. + * @param qualified + * {@code true} if property is qualified by an alias; {@code false} if {@code node} + * represents an unqualified property name. + * @param field String[2] array to hold table alias and field name if found + * @return true if this is the '_multiplex' or PK column. + */ + private boolean generateProperty(FQLAst node, + boolean forceCsPrefix, + boolean qualified, + String[] field) + { if (node.getType() == MULTIPLY) // ASTERISK ? { sb.append(ASTERISK); @@ -1272,7 +1353,17 @@ boolean withAlias = !this.forceNoAlias && !aliasStr.isEmpty(); if (withAlias) { - sb.append(getSqlTableAliasName(aliasStr)); + String aliasName = getSqlTableAliasName(aliasStr); + if (field != null) + { + DmoMeta dmoMeta = aliases.peek().get(aliasName); + field[0] = dmoMeta != null ? dmoMeta.getSqlTableName() : aliasStr; + field[1] = aliasName; + } + else + { + sb.append(aliasName); + } } boolean reserved = false; @@ -1291,8 +1382,8 @@ Property propertyAnn = dmoInfo.fields.get(propertyStr); if (propertyAnn == null) { - // analyze the propertyStr to see whether it is a ComputedColumn (used by case - // insensitive character fields) + // analyze the propertyStr to see whether it is a ComputedColumn (used by case insensitive + // character fields) String csPrefix = dialect.getComputedColumnPrefix(true); if (propertyStr.startsWith(csPrefix)) { @@ -1318,21 +1409,29 @@ } sb.append(propertyStr); // we do not have information on this property. Is this a surrogate? - warn("Column not found for '" + propertyStr + "'. Using java name instead." , property); + warn("Property '" + propertyStr + + "' not found to extract column name. Assuming it is a column name already.", property); } else { reserved = (propertyAnn instanceof ReservedProperty) && (Session.PK.equals(propertyStr) || ReservedProperty._MULTIPLEX.name.equals(propertyStr)); - if (withAlias) - { - sb.append("."); - } - if (prefixed || forceCsPrefix) - { - sb.append(dialect.getComputedColumnPrefix(propertyAnn.caseSensitive)); - } - sb.append(propertyAnn.column); + String prefix = (prefixed || forceCsPrefix) ? + dialect.getComputedColumnPrefix(propertyAnn.caseSensitive) : ""; + + if (field != null) + { + field[2] = prefix + propertyAnn.column; + } + else + { + if (withAlias) + { + sb.append("."); + } + sb.append(prefix); + sb.append(propertyAnn.column); + } } } else @@ -1356,7 +1455,7 @@ if (NO_ALIAS.equals(aliasStr)) { HashMap innerScope = aliases.peek(); - if (innerScope.size() == 1) + if (innerScope != null && innerScope.size() == 1) { return innerScope.entrySet().iterator().next().getValue(); } @@ -1434,6 +1533,7 @@ */ private void generateExpression(FQLAst node) { + ContainsRewriteData aux = new ContainsRewriteData(); AstWalkListener awl = new AstWalkListener() { /** @@ -1454,11 +1554,93 @@ { case IN: case LPARENS: - case FUNCTION: case CAST: sb.append(")"); break; - + case FUNCTION: + if (aux.nestedInContains > 0 ) + { + aux.nestedInContains--; + if (aux.nestedInContains == 0) + { + queryWasRewritten = true; + List args = new ArrayList<>(); + boolean placeHolder = "?".equals(aux.expr); + String exp = placeHolder ? + ((character)queryParams.parameters[aux.narg]).toStringMessage() : + aux.expr; + if (!"_temp".equals(schema)) + { + sb.append("("); + containsQuery(sb, aux.tblName, aux.tblAlias, aux.fldName, exp, args); + sb.append(")"); + if (placeHolder) + { + queryParams.replaceParam(aux.narg, args); + paramCount = paramCount - 1 + args.size(); + aux.narg = aux.narg - 1 + args.size(); + } + else + { + queryParams.insertParam(aux.narg, args); + paramCount = paramCount + args.size(); + aux.narg = aux.narg + args.size(); + } + } + else + { + DmoMeta meta = getScopedDmoInfo(aux.tblName); + if (meta == null) + { + throw new IllegalStateException("No meta for " + aux.tblName); + } + Property fp = meta.fields.get(aux.fldName); + int extent = fp.extent; + boolean caseSensitive = fp.caseSensitive; + StringBuilder contains = new StringBuilder("contains_1("); + if (!caseSensitive) + { + contains.append("upper("); + } + contains.append(aux.fldName); + if (!caseSensitive) + { + contains.append(")"); + } + contains.append(", "); + if (placeHolder) + { + contains.append("?"); + } + else + { + contains.append("\"").append(aux.expr).append("\""); + } + contains.append(")"); + if (extent == 0) + { + sb.append(contains); + } + else + { + sb.append("(").append(PK). + append(" in (select distinct parent__id from "). + append(meta.sqlTable).append("__").append(String.valueOf(extent)). + append(" where ").append(contains).append("))"); + + } + } + aux.tblName = null; + aux.tblAlias = null; + aux.fldName = null; + aux.expr = null; + } + } + else + { + sb.append(")"); + } + break; case TERNARY: sb.append(") end"); break; @@ -1570,7 +1752,10 @@ switch (ast.getType()) { case FUNCTION: - sb.append(", "); + if (aux.nestedInContains == 0) + { + sb.append(", "); + } break; case CAST: @@ -1623,11 +1808,20 @@ switch (ttype) { case SUBST: + sb.append("?"); + break; case POSITIONAL: - sb.append("?"); + aux.narg++; + if (aux.nestedInContains == 1) + { + aux.expr = "?"; + } + else + { + sb.append("?"); + } break; - case FUNCTION: case CAST: sb.append(text); sb.append("("); @@ -1637,6 +1831,34 @@ } break; + case FUNCTION: + if (!dialect.useUdf4Contains()) + { + if (dialect.isUdfContains(text) || (aux.nestedInContains > 0)) + { + aux.nestedInContains++; + } + else + { + sb.append(text); + sb.append("("); + if (next.getNumImmediateChildren() == 0) + { + sb.append(")"); + } + } + } + else + { + sb.append(text); + sb.append("("); + if (next.getNumImmediateChildren() == 0) + { + sb.append(")"); + } + } + break; + case TERNARY: sb.append("case when "); break; @@ -1706,7 +1928,18 @@ case ALIAS: if (next.getParent().getType() != FROM) { - generateProperty(next, false, true); + if (aux.nestedInContains > 0) + { + String [] fld = new String[3]; + generateProperty(next, false, true, fld); + aux.tblName = fld[0]; + aux.tblAlias = fld[1]; + aux.fldName = fld[2]; + } + else + { + generateProperty(next, false, true); + } } // otherwise already processed with the FROM parent break; @@ -1723,6 +1956,16 @@ // do not output this, it was / will be used with the alias in a FROM node break; + case STRING: + if (aux.nestedInContains == 1) + { + aux.expr = text.substring(1, text.length() - 1); + } + else + { + sb.append(text); + } + break; default: sb.append(text); break; @@ -1978,55 +2221,62 @@ Aast grandFather = ast.getParent().getParent(); if (grandFather.getType() == ALIAS) { - DmoMeta dmoMeta = aliasesMap.get(grandFather.getText()); - String extentName = ast.getParent().getText(); - Property extProperty = dmoMeta.fields.get(extentName); - if (extProperty == null && extentName.startsWith("__")) + Aast contains = insideContains(grandFather); + if (contains != null) { - warn("Failed to process extent ALIAS. " + - "Probable caused by a WORD index which is not supported by FWD yet.", (FQLAst) ast); - - extProperty = dmoMeta.fields.get(extentName.substring(3)); - if (extProperty == null) - { + ast.remove(); + } + else { + DmoMeta dmoMeta = aliasesMap.get(grandFather.getText()); + String extentName = ast.getParent().getText(); + Property extProperty = dmoMeta.fields.get(extentName); + if (extProperty == null && extentName.startsWith("__")) + { + warn("Failed to process extent ALIAS. " + + "Probable caused by a WORD index which is not supported by FWD yet.", (FQLAst) ast); + + extProperty = dmoMeta.fields.get(extentName.substring(3)); + if (extProperty == null) + { + return; + } + } + // if the extent property was denormalized, treat it as a simple field + int extent = extProperty.index > 0 ? 0 : extProperty.extent; + if (extent == 0) + { + warn("[] syntax for a non extent property.", (FQLAst) grandFather); return; } - } - // if the extent property was denormalized, treat it as a simple field - int extent = extProperty.index > 0 ? 0 : extProperty.extent; - if (extent == 0) - { - warn("[] syntax for a non extent property.", (FQLAst) grandFather); - return; - } - FQLAst subscriptAst = (FQLAst) ast.getFirstChild(); - String extIdx = subscriptAst.getText(); - String extIdxLit = extIdx; - if (subscriptAst.getType() != NUM_LITERAL) - { - // make the extent index unique - to allow multiple indexed properties with complex exprs - extIdx = "d" + (complexSubscript++); - extIdxLit = "?"; - } - String grandfatherSqlAlias = getSqlTableAliasName(grandFather.getText()); - String newSqlAlias = grandfatherSqlAlias + "_" + extent + "x" + extIdx + "_"; // aliases(grandfather) - aliasesMap.putIfAbsent(newSqlAlias, new CompositeDmoInfo(dmoMeta, extent)); - if (innerJoins != null) - { - innerJoins.putIfAbsent(newSqlAlias, grandfatherSqlAlias); - } - if (subscriptAst.getType() == NUM_LITERAL) - { - staticSubscripts.add((FQLAst) grandFather); - } - else - { - dynamicSubscripts.add((FQLAst) grandFather); - } - grandFather.putAnnotation("parentAlias", grandfatherSqlAlias); - grandFather.putAnnotation("subscript", extIdxLit); - grandFather.setText(newSqlAlias); - ast.remove(); + FQLAst subscriptAst = (FQLAst) ast.getFirstChild(); + String extIdx = subscriptAst.getText(); + String extIdxLit = extIdx; + if (subscriptAst.getType() != NUM_LITERAL) + { + // make the extent index unique - to allow multiple indexed properties with complex exprs + extIdx = "d" + (complexSubscript++); + extIdxLit = "?"; + } + String grandfatherSqlAlias = getSqlTableAliasName(grandFather.getText()); + String newSqlAlias = grandfatherSqlAlias + "_" + extent + "x" + extIdx + "_"; // aliases(grandfather) + aliasesMap.putIfAbsent(newSqlAlias, new CompositeDmoInfo(dmoMeta, extent)); + if (innerJoins != null) + { + innerJoins.putIfAbsent(newSqlAlias, grandfatherSqlAlias); + } + if (subscriptAst.getType() == NUM_LITERAL) + { + staticSubscripts.add((FQLAst) grandFather); + } + else + { + dynamicSubscripts.add((FQLAst) grandFather); + } + grandFather.putAnnotation("parentAlias", grandfatherSqlAlias); + grandFather.putAnnotation("subscript", extIdxLit); + grandFather.setText(newSqlAlias); + ast.remove(); + } } else { @@ -2035,6 +2285,28 @@ } } + /** + * Check if the given Ast is inside a CONTAINS UDF call. + * + * @param grandFather + * AST to be checked. + * + * @return CONTAINS UDF call AST or null + */ + private Aast insideContains(Aast node) + { + Aast parent = node.getParent(); + while (parent != null && parent.getType() == FUNCTION) + { + if (dialect.isUdfContains(parent.getText())) + { + return parent; + } + parent = parent.getParent(); + } + return null; + } + @Override public void nextChild(Aast ast, int index) { @@ -2255,6 +2527,125 @@ } /** + * Generate a replacement for the CONTANS UDF call. + * + * @param sb + * output buffer + * @param tblName + * table name + * @param tblAlias + * table alias + * @param field + * field name + * @param expr + * CONTAINS operator argument value + * @param args + * holder for the generated query parameters + */ + public void containsQuery(StringBuilder sb, + String tblName, + String tblAlias, + String field, + String expr, + List args) + { + List> cnf = new LogicalExpressionConverter(expr, false).toCNF(false); + if (cnf.isEmpty()) + { + sb.append("1 = 0"); + return; + } + WordIndexData indexData = MetadataManager.getWordTableName(schema, tblName, field); + sb.append(tblAlias).append(".").append(PK).append(" in (\n \tselect distinct "). + append(PK).append(" from "). + append(tblName).append(" t\n"); + if (indexData.extentTableName != null) + { + sb.append("\t\tjoin "). + append(indexData.extentTableName). + append(" e on (e.parent__id = t.").append(PK).append(")\n"); + } + int joinN = 1; + for (List andTerm: cnf) + { + String walias = "w"+joinN++; + sb.append("\t\tjoin "). + append(indexData.wordTableName).append(" as ").append(walias). + append(" on (").append(walias); + if (indexData.extentTableName != null) + { + sb.append(".parent__id = e.parent__id and "). + append(walias).append(".list__index = e.list__index"); + } + else + { + sb.append(".parent__id = t.").append(PK); + } + sb.append(" and ("); + onClause(sb, walias, andTerm, args, indexData.caseSensitive); + sb.append("))\n"); + } + sb.append(")"); + } + + /** + * Generate an ON clause for a CONTAINS UDF call replacement query. + * + * @param sb + * output buffer + * @param walias + * word table alias + * @param andTerm + * list of tokens in the AND term of the CNF + * @param args + * holder for the generated query parameters + * @param caseSensitive + * Flag indicating that field is case sensitive + */ + private static void onClause(StringBuilder sb, + String walias, List andTerm, List args, boolean caseSensitive) + { + // NB: uppercase at the database side + String placeHolder = caseSensitive ? " ?" : " UPPER(?)"; + Iterator tokens = andTerm.iterator(); + EToken token = tokens.next(); // always exists + sb.append(walias).append(".word "). + append(op(token, args)).append(placeHolder); + while (tokens.hasNext()) + { + token = tokens.next(); // always exists + sb.append(" or ").append(walias).append(".word "). + append(op(token, args)).append(placeHolder); + } + } + + /** + * Generate a SQL condition ('=' or 'LIKE') for an operation on the CNF. + * + * @param token + * CNF token + * @param args + * holder for the generated query parameters + * + * @return a SQL condition ('=' or 'LIKE') for an operation on the CNF + */ + private static String op(EToken token, List args) + { + if (token instanceof Equals) + { + args.add(((Equals)token).getValue()); + return "="; + } + if (token instanceof StartsWith) + { + args.add(((StartsWith)token).getValue()+ "%"); + return "like"; + } + // CNF for a valid CONTAINS argument does not contain negations + throw new IllegalStateException("Unexpected CNF token:" + token); + } + + /** * The immutable structure for a composite table. It adds only the extent as internal data and * foreign key to its {@code parent} DMO table. */ @@ -2295,8 +2686,41 @@ { return sqlTableName; } - } - + + /** + * Get parent DmoMeta + * @return parent DmoMeta + */ + public DmoMeta getParentDmoInfo() + { + return parentDmoInfo; + } + } + + /** + * Auxiliary date used for CONTAINS UDF call re-writing + */ + private static class ContainsRewriteData + { + /** table name */ + public String tblName = null; + /** table alias */ + public String tblAlias = null; + /** field name */ + public String fldName = null; + /** CONTAINS UDF second argument value */ + public String expr = null; + /** placeholders found so far */ + int narg = -1; + /** depth of nester function's calls */ + int nestedInContains = 0; + } + + /** + * Test program + * @param args + * command line args + */ public static void main(String[] args) { FqlToSqlConverter fql2sql = getInstance(new P2JH2Dialect(), "fwd"); === modified file 'src/com/goldencode/p2j/persist/orm/OrmUtils.java' --- src/com/goldencode/p2j/persist/orm/OrmUtils.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/OrmUtils.java 2021-01-29 11:56:23 +0000 @@ -2,12 +2,13 @@ ** Module : OrmUtils.java ** Abstract : ORM-related utility methods. ** -** Copyright (c) 2020, Golden Code Development Corporation. +** Copyright (c) 2020-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 OM 20200731 First revision. ** 002 CA 20200921 Added getField(record, index). ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** AIL 20201210 Added setAllFields for bulk copy of fields. */ /* @@ -169,6 +170,20 @@ } /** + * Sets all fields on a record based on a provided record. The low-level values are directly assigned + * in the {@code data} array of the record. + * + * @param src + * The source record from which the data will be copied. + * @param dst + * The destination record to which the data will be copied. + */ + public static boolean setAllFields(BaseRecord src, BaseRecord dst) + { + return dst.setAllDatum(src); + } + + /** * Get the field on the given index, from the specified record. * * @param record === modified file 'src/com/goldencode/p2j/persist/orm/Persister.java' --- src/com/goldencode/p2j/persist/orm/Persister.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/Persister.java 2021-01-13 21:04:41 +0000 @@ -9,6 +9,8 @@ ** the DMO's metadata. ** OM 20191120 Some fixes in insert() method. Added delete() method. ** 002 OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** OM 20201007 Replaced inlined column name with TemporaryBuffer.MULTIPLEX_FIELD_NAME. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -598,7 +600,7 @@ if (extent == 0 && dmo instanceof TempRecord) // TempTableRecord { - sqlBuilder.append(" and _multiplex=?"); + sqlBuilder.append(" and ").append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append("=?"); sqlPar.add(ReservedProperty.ID_MULTIPLEX); // ((TempRecord) dmo)._multiplex() } @@ -663,7 +665,7 @@ typeHandler = TypeManager.getTypeHandler(Long.class); break; case ReservedProperty.ID_ERROR_FLAG: - par = ((TempRecord) dmo)._errorFlag(); + par = ((TempRecord) dmo)._errorFlags(); typeHandler = TypeManager.getTypeHandler(Integer.class); break; case ReservedProperty.ID_MULTIPLEX: === modified file 'src/com/goldencode/p2j/persist/orm/PooledDataSourceProvider.java' --- src/com/goldencode/p2j/persist/orm/PooledDataSourceProvider.java 2020-07-03 22:49:48 +0000 +++ src/com/goldencode/p2j/persist/orm/PooledDataSourceProvider.java 2020-10-16 09:11:30 +0000 @@ -2,10 +2,11 @@ ** Module : PooledDataSourceProvider.java ** Abstract : Implementation of DataSourceProvider that offers a pooled DataSource. ** -** Copyright (c) 2019, Golden Code Development Corporation. +** Copyright (c) 2019-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --------------------------------Description---------------------------------- ** 001 ECF 20191001 Created initial version with basic runtime support. +** 002 AIL 20201016 Set c3p0 options through setters instead of system properties. */ /* @@ -64,12 +65,16 @@ package com.goldencode.p2j.persist.orm; import java.beans.*; +import java.lang.reflect.*; import java.util.*; +import java.util.logging.*; + import javax.sql.DataSource; import com.mchange.v2.c3p0.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.orm.types.*; +import com.goldencode.p2j.util.*; /** * TBA @@ -77,6 +82,25 @@ class PooledDataSourceProvider implements DataSourceProvider { + /** A collection of setters for a ComboPooledDataSource */ + private static Map cpdsSetters = new HashMap<>(); + + /** Logger */ + private static final Logger LOG = LogHelper.getLogger(PooledDataSourceProvider.class.getName()); + + /** Initialize the cpdsSetters collection */ + static + { + Method[] methods = ComboPooledDataSource.class.getMethods(); + for (Method m : methods) + { + if (m.getName().substring(0, "set".length()).equals("set")) + { + cpdsSetters.put(m.getName().substring("set".length()).toLowerCase(), m); + } + } + } + PooledDataSourceProvider() { } @@ -119,8 +143,37 @@ for (Map.Entry next : settings.getAllForCategory("c3p0").entrySet()) { String key = next.getKey(); - String value = String.valueOf(next.getValue()); - System.setProperty(key, value); + Object value = next.getValue(); + + try + { + Method setter = cpdsSetters.get(key.substring("c3p0.".length()).toLowerCase()); + if (setter == null) + { + if (LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, "Can't find a setter for the specified c3p0 parameter " + key); + } + } + else + { + setter.invoke(ds, value); + } + } + catch (IllegalArgumentException e) + { + if (LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, "The provided value is not suitable for the c3p0 parameter " + key); + } + } + catch (IllegalAccessException | InvocationTargetException e) + { + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, "Failed to set a value for the c3p0 parameter " + key); + } + } } // setup the blob and clob creators === modified file 'src/com/goldencode/p2j/persist/orm/Property.java' --- src/com/goldencode/p2j/persist/orm/Property.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/Property.java 2021-01-21 22:49:45 +0000 @@ -6,6 +6,8 @@ ** ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------- ** 001 OM 20200110 First revision. +** 002 SVL 20201111 Convert "reserved null" to true null for LABEL and COLUMN-LABEL. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -232,7 +234,7 @@ public final boolean _isDatetimeTz; /** The method which was annotated. This is only visible in orm package. */ - final Method annMethod; + public final Method annMethod; /** * The constructor that initialize this immutable object. @@ -257,9 +259,9 @@ this.initial = source.initial().intern(); this.initial_sa = source.initial_sa(); this.initialNull = source.initialNull(); - this.label = source.label(); + this.label = convertReservedNull(source.label()); this.label_sa = source.label_sa(); - this.columnLabel = source.columnLabel(); + this.columnLabel = convertReservedNull(source.columnLabel()); this.columnLabel_sa = source.columnLabel_sa(); this.mandatory = source.mandatory(); this.order = source.order(); @@ -282,15 +284,38 @@ this.viewAs = source.viewAs(); this.canRead = source.canRead(); this.canWrite = source.canWrite(); - + annMethod = annotatedMethod; this._fwdType = annotatedMethod.getReturnType(); - _ablType = _fwdType.getName().toLowerCase(); // TODO: fix this hack _isCharacter = _fwdType == character.class; _isDatetimeTz = _fwdType == datetimetz.class; + _ablType = getAblType(); } - Property(int id, String name, String legacy, String column, boolean mandatory, Class type) + /** + * Creates a new {@code Property} object. This is a constructor dedicated to building reserved properties + * which are not declared in DMO interface. + * + * @param id + * The id of this field in the sequence of the fields of the buffer. + * @param name + * The name of the DMO property. + * @param legacy + * The name of the legacy field associated with the DMO property. + * @param column + * The name of the column in the SQL database, associated with the DMO property. + * @param mandatory + * Flag for not-null fields. + * @param initialNull + * The property has {@code null} as default value. + */ + Property(int id, + String name, + String legacy, + String column, + boolean mandatory, + Class type, + boolean initialNull) { this.id = id; this.name = name.intern(); @@ -300,7 +325,7 @@ this.index = 0; this.format = ""; this.initial = ""; - this.initialNull = false; + this.initialNull = initialNull; this.label = ""; this.columnLabel = ""; this.mandatory = mandatory; @@ -330,11 +355,59 @@ this.canWrite = "*"; this.desc = null; this.width = 0; - + annMethod = null; // no method for these properties this._fwdType = type; - _ablType = type.getName().toLowerCase(); // TODO: fix this hack - _isCharacter = type == character.class; + _isCharacter = (type == character.class); _isDatetimeTz = false; // no dtzs here + _ablType = getAblType(); + } + + /** + * Converts "reserved null" to true null. Returns null if the value + * equals "reserved null" for {@link com.goldencode.p2j.persist.annotation.Property} annotation. Otherwise + * returns the value itself. + * + * @param value + * Value to check. + * + * @return see above. + */ + private String convertReservedNull(String value) + { + return com.goldencode.p2j.persist.annotation.Property.NULL_STRING.equals(value) ? null : value; + } + + /** + * Computes the ABL type name from the Java type. Usually this is the same as java class (because FWD makes + * an exception here from standard class naming), but there is also an exception (@code datetime-tz) + * because the character {@code -} is not permitted in a Java identifier. + * + * @return The ABL type name mapped by the specified {@code type}. + */ + private String getAblType() + { + // a bit of a hack to get the proper ABL type name + String strType = _isDatetimeTz ? "datetime-tz" : _fwdType.getSimpleName().toLowerCase(); + return strType.intern(); + } + + /** + * Obtain a short description of the object used in debugging. + * + * @return a short description of the object used in debugging. + */ + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("ORM-Property{").append(name); + if (extent > 0) + { + sb.append("[").append(extent).append("]"); + } + sb.append(":").append(_ablType).append(" OE=").append(legacy) + .append(" SQL=").append(column).append(" id=").append(id).append("}"); + return sb.toString(); } } === modified file 'src/com/goldencode/p2j/persist/orm/PropertyMeta.java' --- src/com/goldencode/p2j/persist/orm/PropertyMeta.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/PropertyMeta.java 2021-01-13 21:04:41 +0000 @@ -8,6 +8,9 @@ ** 001 ECF 20191001 First revision. ** OM 20191110 Added property type support and toString(). ** 002 OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** AIL 20201210 Added checker for reserved properties. +** 20201216 Added initial value getter. +** OM 20201120 Fixed value returned by getDecimals() method. */ /* @@ -240,13 +243,17 @@ } /** - * Obtain numeric scale (precision in 4GL terms) (applies to {@code decimal} fields only). + * Obtain numeric scale (precision in 4GL terms) (applies to {@code decimal} fields only) ad defined in + * the field declaration. The method returns 0 when no such declaration was encountered in ABL source, but + * the default value to be used in this case is 10. * * @return the scale/precision type of the {@code decimal} field. */ public int getDecimals() { - return dmoProperty.scale; + // dmoProperty.scale stores the legacy scale, if defined. If not specified in ABL source, it will + // be 0. But the default value is 10 decimals. + return dmoProperty.scale != 0 ? dmoProperty.scale : 10; } /** @@ -343,6 +350,16 @@ { return dmoProperty.mandatory; } + + /** + * Check if this property meta is for a reserved property. + * + * @return {@code true} if the referenced property is reserved. + */ + public boolean isReserved() + { + return dmoProperty.id < 0; + } /** * Retrieve the original extent of this property, which differs from @extent@ only for denormalized @@ -370,6 +387,16 @@ } /** + * Retrieve the original initial value of this property. + * + * @return The initial value for this property. + */ + public Object getInitialValue() + { + return this.initialValue; + } + + /** * Returns a short text describing this object. Used for debugging. * * @return a short description of this object to be used when debugging. === modified file 'src/com/goldencode/p2j/persist/orm/Query.java' --- src/com/goldencode/p2j/persist/orm/Query.java 2020-09-25 16:00:36 +0000 +++ src/com/goldencode/p2j/persist/orm/Query.java 2021-01-29 19:50:19 +0000 @@ -8,6 +8,7 @@ ** 001 ECF 20191001 Created initial version with stubbed methods. ** OM 20191108 Added method implementations. ** ECF 20200419 Added getSQLQuery for UNDO processing. +** 002 IAS 20201125 Wrap parameters in the auxiliary class */ /* @@ -66,6 +67,11 @@ package com.goldencode.p2j.persist.orm; import java.util.*; +import java.util.function.*; +import java.util.regex.*; +import java.util.stream.*; + +import com.goldencode.p2j.util.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.meta.*; @@ -75,13 +81,19 @@ */ public class Query { + /** Caching lambda */ + Consumer cache; + /** The query as a FQL (FWD Query Language) statement. */ private final String fql; /** Flags a read-only query. */ private boolean readOnly = false; - /** + /** Flags a query which was re-written */ + private boolean wasRewritten = false; + + /** * The maximum number of results accepted by the (SELECT) query. Used with {@code firstResult} * to configure result pagination. */ @@ -92,19 +104,13 @@ * with {@code firstResult} to configure result pagination. */ private int firstResult = -1; - - /** The set of positional parameters */ - private Object[] parameters = null; - - /** - * The index of the maximum parameter set. It is assumed this is the last positional parameter - * for the FQL query. - */ - private int maxParamIndex = -1; - + /** The number of parameter as detected after parsing the FQL statement. */ private int paramCount = 0; + /** Query parameters' data */ + private final QueryParams params = new QueryParams(); + /** The cached SQL query. */ private SQLQuery sqlQuery = null; @@ -117,30 +123,27 @@ /** * Creates a new {@code Query} object. - * + * @param cache + * query cache * @param fql * The query as a FQL (FWD Query Language) statement. */ - public Query(String fql) + public Query(Consumer cache, String fql) { + this.cache = cache; this.fql = fql; } /** - * Sets the value of a parameter for this query. This is done before parsing the HQL so the - * index of the parameter is NOT checked. - * - * @param index - * The index of the parameter to be set (0-base). - * @param arg - * The parameter value. + * Check if the query was re-written + * + * @return true if the query was re-written */ - public void setParameter(int index, Object arg) + public boolean isWasRewritten() { - ensureParameterSize(index); - parameters[index] = arg; + return wasRewritten; } - + /** * Configures the maximum number of rows returned by this query. Depending on the SQL dialect * from the {@code session}, the value is injected as the parameter of SQL {@code limit} @@ -182,6 +185,20 @@ } /** + * Sets the value of a parameter for this query. This is done before parsing the HQL so the + * index of the parameter is NOT checked. + * + * @param index + * The index of the parameter to be set (0-base). + * @param arg + * The parameter value. + */ + public void setParameter(int index, Object arg) + { + params.setParameter(index, arg); + } + + /** * Returns a scrollable list of rows of type {@code } by converting the {@code fql} to SQL * and executing the query against the SQL server. * @@ -218,7 +235,9 @@ * The {@code Session} to be used. * * @return A row of type {@code } from SQL server, as specified by the {@code fql} query. - * + * + * @throws UniqueResultException + * if the result set contains not an exactly one row. * @throws PersistenceException * when an error occurred while performing the requested operations. */ @@ -291,43 +310,7 @@ return sqlQuery; } - /** - * Makes sure the array that holds the parameters for this query is large enough to accommodate - * the {@code fitIndex}-th parameter. If the index fits the current size nothing is performed. - * Otherwise the array is reallocated and the existing parameters are copied to their fixed - * positions. To improve performance, more space is allocated so that this operation will not - * happen at each method call. - * - * @param fitIndex - * The index of the latest added parameter. - */ - private void ensureParameterSize(int fitIndex) - { - if (maxParamIndex < fitIndex) - { - maxParamIndex = fitIndex; - } - - if (parameters == null) - { - parameters = new Object[Math.max(5, fitIndex + 1)]; // TODO: optimize this ? - return; - } - - if (parameters.length > fitIndex) - { - // nothing to do - return; - } - - // create a new array and copy from previously set [parameters] to new location - Object[] newParam = new Object[1 + fitIndex * 3 / 2]; // TODO: optimize this ? - System.arraycopy(parameters, 0, newParam, 0, parameters.length); - - this.parameters = newParam; - } - - /** + /** * Creates the delegating SQL query. The {@code fql} statement is converted to SQL and a * {@code SQLQuery} is created from current session. The pagination are configured and the * eventual parameters are set. When finished, the SQL query is returned. @@ -347,36 +330,42 @@ { if (sqlQuery == null) { - FqlToSqlConverter fql2sql = FqlToSqlConverter.getInstance(dialect, schema); + FqlToSqlConverter fql2sql = FqlToSqlConverter.getInstance(dialect, schema, params); rowStructure = new ArrayList<>(2); // this should be enough for most cases - sqlQuery = Session.createSQLQuery(fql2sql.toSQL(fql, maxResults, firstResult, rowStructure)); + String sql = fql2sql.toSQL(fql, maxResults, firstResult, rowStructure); paramCount = fql2sql.getLastConversionParamCount(); + wasRewritten = fql2sql.isQueryWasRewritten(); + if (!wasRewritten) + { + cache.accept(this); + } + sqlQuery = Session.createSQLQuery(sql); } // [paramCount] includes optional [maxResults], [firstResult] positions - ensureParameterSize(paramCount); + params.ensureParameterSize(paramCount); int paging = paramCount; if (firstResult > 0) { // add [firstResult] at the end of parameter list - parameters[--paging] = firstResult; + params.parameters[--paging] = firstResult; } if (maxResults > 0) { // add [maxResults] at the end of parameter list - parameters[--paging] = maxResults; + params.parameters[--paging] = maxResults; } for (int i = 0; i < paramCount; i++) { - sqlQuery.setParameter(i, parameters[i]); + sqlQuery.setParameter(i, params.parameters[i]); } return sqlQuery; } - + /** * Get the dialect of the database the session is connected to. * @@ -403,4 +392,125 @@ Database db = session.getDatabase(); return db.isMeta() ? MetadataManager.META_SCHEMA : db.getName(); } + + /** + * Parameters' data holder + */ + public static class QueryParams + { + /** The set of positional parameters */ + public Object[] parameters = null; + + /** + * The index of the maximum parameter set. It is assumed this is the last positional parameter + * for the FQL query. + */ + public int maxParamIndex = -1; + + /** + * Makes sure the array that holds the parameters for this query is large enough to accommodate + * the {@code fitIndex}-th parameter. If the index fits the current size nothing is performed. + * Otherwise the array is reallocated and the existing parameters are copied to their fixed + * positions. To improve performance, more space is allocated so that this operation will not + * happen at each method call. + * + * @param fitIndex + * The index of the latest added parameter. + */ + public void ensureParameterSize(int fitIndex) + { + if (maxParamIndex < fitIndex) + { + maxParamIndex = fitIndex; + } + + if (parameters == null) + { + parameters = new Object[Math.max(5, fitIndex + 1)]; // TODO: optimize this ? + return; + } + + if (parameters.length > fitIndex) + { + // nothing to do + return; + } + + // create a new array and copy from previously set [parameters] to new location + Object[] newParam = new Object[1 + fitIndex * 3 / 2]; // TODO: optimize this ? + System.arraycopy(parameters, 0, newParam, 0, parameters.length); + + this.parameters = newParam; + } + + /** + * Sets the value of a parameter for this query. This is done before parsing the HQL so the + * index of the parameter is NOT checked. + * + * @param index + * The index of the parameter to be set (0-base). + * @param arg + * The parameter value. + */ + public void setParameter(int index, Object arg) + { + ensureParameterSize(index); + parameters[index] = arg; + } + + /** + * Replace parameter + * @param narg + * parameter number + * @param args + * list of replacement parameters + */ + public void replaceParam(int narg, List args) + { + int oldnPar = parameters.length; + Object[] newParams = new Object[oldnPar - 1 + args.size()]; + int np = 0; + for (int i = 0; i < narg; i++) + { + newParams[np++] = parameters[i]; + } + for (String s: args) + { + newParams[np++] = new character(s); + } + for (int i = narg + 1; i < oldnPar; i++) + { + newParams[np++] = parameters[i]; + } + parameters = newParams; + maxParamIndex = maxParamIndex - 1 + args.size(); + } + /** + * Insert parameter(s) + * @param narg + * parameter number after which to insert + * @param args + * list of added parameters + */ + public void insertParam(int narg, List args) + { + int oldnPar = parameters == null ? 0 : parameters.length; + Object[] newParams = new Object[oldnPar + args.size()]; + int np = 0; + for (int i = 0; i <= narg; i++) + { + newParams[np++] = parameters[i]; + } + for (String s: args) + { + newParams[np++] = new character(s); + } + for (int i = narg + 1; i < oldnPar; i++) + { + newParams[np++] = parameters[i]; + } + parameters = newParams; + maxParamIndex = maxParamIndex + args.size(); + } + } } === modified file 'src/com/goldencode/p2j/persist/orm/RecordMeta.java' --- src/com/goldencode/p2j/persist/orm/RecordMeta.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/orm/RecordMeta.java 2020-10-09 21:13:00 +0000 @@ -12,6 +12,7 @@ ** performance optimization. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201007 Replaced inlined column name with TemporaryBuffer.MULTIPLEX_FIELD_NAME. */ /* @@ -550,7 +551,7 @@ if (temporary) { - sql.append(" and _multiplex = ?"); + sql.append(" and ").append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ?"); } BitSet uIndex = uniqueIndices[k]; @@ -593,7 +594,8 @@ for (int k = 0; k < len; k++) { StringBuilder sql = new StringBuilder(); - sql.append("delete from ").append(sqlTableName).append(" where _multiplex = ?"); + sql.append("delete from ").append(sqlTableName).append(" where ") + .append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ?"); BitSet uIndex = uniqueIndices[k]; for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1)) === modified file 'src/com/goldencode/p2j/persist/orm/ReservedProperty.java' --- src/com/goldencode/p2j/persist/orm/ReservedProperty.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/ReservedProperty.java 2021-01-13 21:04:41 +0000 @@ -9,6 +9,7 @@ ** 001 OM 20200110 Added constants and method implementations. ** 002 CA 20200922 Added getByName(), to resolve the reserved property for a 'hidden field' in 4GL. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields. */ /* @@ -115,39 +116,39 @@ /** The {@code id} reserved (by FWD) property. */ public static final ReservedProperty ID = new ReservedProperty( - ID_PRIMARY_KEY, Session.PK, null, Session.PK, true, Long.class); + ID_PRIMARY_KEY, Session.PK, null, Session.PK, true, Long.class, false); /** The {@code _multiplex} reserved (by FWD) property. */ public static final ReservedProperty _MULTIPLEX = new ReservedProperty( - ID_MULTIPLEX, TemporaryBuffer.MULTIPLEX_FIELD_NAME, null, TemporaryBuffer.MULTIPLEX_FIELD_NAME, true, Integer.class); + ID_MULTIPLEX, TemporaryBuffer.MULTIPLEX_FIELD_NAME, null, TemporaryBuffer.MULTIPLEX_FIELD_NAME, true, Integer.class, false); /** The {@code __error-flag__} reserved (by P4GL) property. */ public static final ReservedProperty _ERRORFLAG = new ReservedProperty( - ID_ERROR_FLAG, TempRecord._ERROR_FLAG, "__error-flag__", TempRecord._ERROR_FLAG, false, integer.class); + ID_ERROR_FLAG, TempRecord._ERROR_FLAG, "__error-flag__", TempRecord._ERROR_FLAG, false, integer.class, true); /** The {@code __origin-rowid__} reserved (by P4GL) property. */ public static final ReservedProperty _ORIGINROWID = new ReservedProperty( - ID_ORIGIN_ROWID, TempRecord._ORIGIN_ROWID, "__origin-rowid__", TempRecord._ORIGIN_ROWID, false, rowid.class); + ID_ORIGIN_ROWID, TempRecord._ORIGIN_ROWID, "__origin-rowid__", TempRecord._ORIGIN_ROWID, false, rowid.class, true); /** The {@code _errorString} reserved (by P4GL) property. */ public static final ReservedProperty _ERRORSTRING = new ReservedProperty( - ID_ERROR_STRING, TempRecord._ERROR_STRING, "__error-string__", TempRecord._ERROR_STRING, false, character.class); + ID_ERROR_STRING, TempRecord._ERROR_STRING, "__error-string__", TempRecord._ERROR_STRING, false, character.class, true); /** The {@code __after-rowid__} reserved (by P4GL) property. */ public static final ReservedProperty _PEERROWID = new ReservedProperty( - ID_PEER_ROWID, TempRecord._PEER_ROWID, "__after-rowid__", TempRecord._PEER_ROWID, false, rowid.class); + ID_PEER_ROWID, TempRecord._PEER_ROWID, "__after-rowid__", TempRecord._PEER_ROWID, false, rowid.class, true); /** The {@code __row-state__} reserved (by P4GL) property. */ public static final ReservedProperty _ROWSTATE = new ReservedProperty( - ID_ROW_STATE, TempRecord._ROW_STATE, "__row-state__", TempRecord._ROW_STATE, false, integer.class); + ID_ROW_STATE, TempRecord._ROW_STATE, "__row-state__", TempRecord._ROW_STATE, false, integer.class, true); /** The {@code list__index} reserved (by FWD) property. */ public static final ReservedProperty LIST__INDEX = new ReservedProperty( - ID_LIST__INDEX, "list__index", "index", "list__index", true, Integer.class); // or integer ? + ID_LIST__INDEX, "list__index", "index", "list__index", true, Integer.class, false); // or integer ? /** The {@code list__index} reserved (by FWD) property. */ public static final ReservedProperty PARENT__ID = new ReservedProperty( - ID_PARENT__ID, "parent__id", "fk_parent", "parent__id", true, Long.class); // or int64 ? + ID_PARENT__ID, "parent__id", "fk_parent", "parent__id", true, Long.class, false); // or int64 ? /** The map of reserved properties which exist in 4GL, but are hidden. */ private static final Map HIDDEN_PROPERTIES = new HashMap<>(); @@ -174,10 +175,18 @@ * The name of the column in the SQL database, associated with the DMO property. * @param mandatory * Flag for not-null fields. + * @param initialNull + * The property has {@code null} as default value. */ - public ReservedProperty(int id, String name, String legacy, String column, boolean mandatory, Class type) + public ReservedProperty(int id, + String name, + String legacy, + String column, + boolean mandatory, + Class type, + boolean initialNull) { - super(id, name, legacy, column, mandatory, type); + super(id, name, legacy, column, mandatory, type, initialNull); } /** === modified file 'src/com/goldencode/p2j/persist/orm/Session.java' --- src/com/goldencode/p2j/persist/orm/Session.java 2020-10-02 22:45:29 +0000 +++ src/com/goldencode/p2j/persist/orm/Session.java 2021-01-29 19:50:19 +0000 @@ -11,6 +11,9 @@ ** 002 ECF 20200910 Verify a record is not stale before saving or caching it. ** OM 20200924 Enforced no-stale-in-cache rule. ** OM 20201002 Use DmoMeta cached information instead of map lookups. +** OM 20201202 Exposed API for removing cached records which were afected by explicit SQL statements +** (these changes invalidate data in cache). +** IAS 20201123 createQuary with delayed caching */ /* @@ -70,9 +73,10 @@ import java.lang.reflect.*; import java.sql.*; -import javax.sql.DataSource; import java.util.*; +import java.util.function.*; import java.util.logging.*; +import javax.sql.DataSource; import com.goldencode.asm.*; import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.directory.*; @@ -283,7 +287,24 @@ */ public static Query createQuery(String fql) { - return new Query(fql); + return createQuery(q -> {}, fql); + } + + /** Flags a warning regarding "subscript on array field in CONTAINS phrase" is suppressed */ + /** + * Creates and returns a {@code Query} based on a FQL statement. The returned object is not + * bounded to any session; the {@code Session} upon which it will be executed will be provided + * at the time of execution. + * + * @param cache + * caching lambda + * @param fql + * The FQL statement that will be executed by the returned {@code Query}. + * @return a {@code SQLQuery} that can be used to execute the specified SQL statement. + */ + public static Query createQuery(Consumer cache, String fql) + { + return new Query(cache, fql); } /** @@ -507,17 +528,17 @@ } if (dmoImplClass == null) - try - { - // normally, [entity] is class dynamically generated by [DmoClass] and loaded by [AsmClassLoader]. - // it will generate [ClassNotFoundException] when queried using the simpler Class.forName() method - dmoImplClass = (Class) Class.forName(key.getTable().toString(), - true, - AsmClassLoader.getInstance()); - } - catch (ClassNotFoundException e) - { - throw new PersistenceException(e); + { + try + { + // normally, [entity] is class dynamically generated by [DmoClass] and loaded by [AsmClassLoader]. + // it will generate [ClassNotFoundException] when queried using the simpler Class.forName() method + dmoImplClass = (Class) Class.forName(key.getTable(), true, AsmClassLoader.getInstance()); + } + catch (ClassNotFoundException e) + { + throw new PersistenceException(e); + } } if (!Record.class.isAssignableFrom(dmoImplClass)) @@ -688,8 +709,11 @@ /** * Delete the DMO's record(s) from the database and remove it from the cache. * + * @param + * DMO type * @param dmo * DMO to be deleted. + * @return true on success * * @throws PersistenceException * if there is a database error, or if a different DMO instance with the same ID as @@ -860,6 +884,54 @@ } /** + * Invalidates all records from cache belonging to a specific table. If provided a multiplex, only + * those records are affected. + * + * @param buf + * A buffer of the table whose records are going to be invalidated. + * @param multiplex + * Optional multiplex used to filter which records are deleted. + */ + public void invalidateRecords(RecordBuffer buf, Integer multiplex) + { + if (cache == null) + { + return; // nothing to do + } + + String tableName = buf.getDmoInfo().getImplementationClass().getName(); + + Set, BaseRecord>> entries = cache.entrySet(); + Set> toDelete = new HashSet<>(); + for (Map.Entry, BaseRecord> entry : entries) + { + if (!entry.getKey().getTable().equals(tableName)) + { + continue; + } + + if (multiplex != null) + { + TempRecord value = (TempRecord) entry.getValue(); + if (value._multiplex().intValue() != multiplex.intValue()) + { + continue; + } + } + + toDelete.add(entry.getKey()); + } + + if (!toDelete.isEmpty()) + { + for (RecordIdentifier ri : toDelete) + { + cache.remove(ri); + } + } + } + + /** * Refresh the given DMO with data from the database, bypassing the cache. The DMO's primary * key is assumed to represent a valid identifier. If no record is found with this primary * key, the existing DMO (if any) with that identifier is removed from the cache, and {@code @@ -905,6 +977,14 @@ // NOTE: Hibernate's Session.merge() saves the record if not done already. FWD ORM does the // same in order to keep the existing code assumptions. // TODO: can we get rid of this? + /** + * @param + * dmo TYPE + * @param dmo + * DMO + * @return saved record + * @throws PersistenceException + */ public T merge(T dmo) throws PersistenceException { @@ -1303,8 +1383,6 @@ /** * Clear the session DMO cache. - * - * @throws PersistenceException */ public void clear() throws PersistenceException === modified file 'src/com/goldencode/p2j/persist/orm/Settings.java' --- src/com/goldencode/p2j/persist/orm/Settings.java 2020-09-18 11:12:02 +0000 +++ src/com/goldencode/p2j/persist/orm/Settings.java 2020-12-20 17:05:05 +0000 @@ -8,6 +8,8 @@ ** 001 ECF 20191001 Created initial version. ORM layer configuration information. ** 002 IAS 20200914 Re-work (de)serialization ** 003 AIL 20200918 Made Settings use the dynamically computed URL for per-session databases. +** 004 AIL 20201016 Increased searching interval for getAllForCategory. +** 005 IAS 20201219 Added 'CPSTREAM' import task argument */ /* @@ -93,6 +95,9 @@ /** Connection password key */ public static final String PASSWORD = "connection.password"; + /** table dump file codepage, optional */ + public static final String CPSTREAM = "dump.cpstream"; + /** Connection driver class key */ public static final String DRIVER = "connection.driver"; @@ -299,7 +304,7 @@ */ public Map getAllForCategory(String qualifier) { - return config.subMap(qualifier + '.', true, qualifier + "._", true); + return config.subMap(qualifier + '.', true, qualifier + ".~", true); } /** === modified file 'src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java' --- src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java 2020-08-14 00:21:10 +0000 +++ src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java 2020-10-22 18:16:39 +0000 @@ -1,13 +1,15 @@ /* ** Module : TempTableDataSourceProvider.java -** Abstract : Provides the context-local data source for temporary database. +** Abstract : Provides the context-local data source for legacy temp-table use. ** -** Copyright (c) 2019, Golden Code Development Corporation. +** Copyright (c) 2019-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --------------------------------Description---------------------------------- ** 001 ECF 20191001 Created initial version with basic runtime support. ** OM 20191202 Implemented getConnection() methods. Using delegation instead of proxy because ** the proxied object lacks the default c'tor. +** 002 AIL 20201020 Added prepared statement cache at connection level. +** ECF 20201022 Javadoc cleanup. */ /* @@ -71,13 +73,20 @@ import java.util.concurrent.*; import java.util.logging.*; import javax.sql.DataSource; +import com.goldencode.cache.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.orm.types.*; import com.goldencode.p2j.security.*; +import com.goldencode.p2j.util.LogHelper; /** - * TBA + * A data source provider which supplies a JDBC connection for legacy temp-table use. The connection is not + * closed until all temp-tables have been dropped explicitly. The connection provided is a hard-coded proxy + * which delegates most service requests to a backing, physical JDBC connection, but intercepts certain + * requests to implement the desired behavior. + *

+ * Implements prepared statement caching in a manner compatible with H2's prepared statement implementation. */ class TempTableDataSourceProvider implements DataSourceProvider @@ -96,9 +105,16 @@ return new DataSourceImpl(); } + /** + * Implementation of the {@code DataSource} interface, which maintains context-local data for each + * private temp-table connection. + */ private static class DataSourceImpl implements DataSource { + /** Logger */ + private static final Logger LOG = LogHelper.getLogger(DataSourceImpl.class.getName()); + /** * Attempts to establish a connection with the data source of the {@code _temp} database. * Uses the credentials from the {@code DatabaseManager} configuration. @@ -168,20 +184,69 @@ Dialect dialect = Dialect.getDialect(settings); TypeManager.addBlobCreator(url, dialect::blobCreator); TypeManager.addClobCreator(url, dialect::clobCreator); + + ctx.psCache = new LRUCache<>(100); + ctx.psCache.addCacheExpiryListener(ctx); } if (ctx.proxyConn == null) { - ctx.proxyConn = new Connection() { + ctx.proxyConn = new Connection() + { public void close() throws SQLException { DataSourceImpl.this.close(); } + public PreparedStatement prepareStatement(String sql) + throws SQLException + { + // prepare using default result set type and concurrency + return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } + + public PreparedStatement prepareStatement(String sql, + int resultSetType, + int resultSetConcurrency) + throws SQLException + { + PSKey key = new PSKey(sql, resultSetType, resultSetConcurrency); + UnclosablePreparedStatement ps = ctx.psCache.get(key); + + if (ps == null || ps.isClosed()) + { + if (ps != null) + { + if (LOG.isLoggable(Level.SEVERE)) + { + LOG.log(Level.SEVERE, "Closed statements shouldn't be cached."); + } + } + + PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency); + ps = new UnclosablePreparedStatement(raw, false); + ctx.psCache.put(key, ps); + } + else if (ps.isCheckedOut()) + { + // the prepared statement is already in use + // create another one which will not be cached + PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency); + ps = new UnclosablePreparedStatement(raw, true); + + if (LOG.isLoggable(Level.FINE)) + { + LOG.log(Level.FINE, "Multiply-cached PreparedStatement: " + sql); + } + } + + ps.checkOut(); + return ps; + } + public T unwrap(Class iface) throws SQLException { return ctx.physicalConn.unwrap(iface); } public boolean isWrapperFor(Class iface) throws SQLException { return ctx.physicalConn.isWrapperFor(iface); } public Statement createStatement() throws SQLException { return ctx.physicalConn.createStatement(); } - public PreparedStatement prepareStatement(String sql) throws SQLException { return ctx.physicalConn.prepareStatement(sql); } public CallableStatement prepareCall(String sql) throws SQLException { return ctx.physicalConn.prepareCall(sql); } public String nativeSQL(String sql) throws SQLException { return ctx.physicalConn.nativeSQL(sql); } public void setAutoCommit(boolean autoCommit) throws SQLException { ctx.physicalConn.setAutoCommit(autoCommit); } @@ -200,8 +265,6 @@ public void clearWarnings() throws SQLException { ctx.physicalConn.clearWarnings(); } public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { return ctx.physicalConn.createStatement(resultSetType, resultSetConcurrency); } - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { - return ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency); } public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return ctx.physicalConn.prepareCall(sql, resultSetType, resultSetConcurrency); } public Map> getTypeMap() throws SQLException { @@ -284,6 +347,13 @@ throw new SQLFeatureNotSupportedException(); } + /** + * Intercept close requests to close the physical database connection only when there is no temp-table + * which still needs it. + * + * @throws SQLException + * if there is an error closing the connection. + */ public void close() throws SQLException { @@ -293,9 +363,11 @@ { ctx.physicalConn.close(); ctx.physicalConn = null; + ctx.psCache = null; } } + /** Context local variable to hold context-specific data */ private static final ContextLocal context = new ContextLocal() { /** @@ -349,13 +421,145 @@ } }; + /** + * Context-specific data. + */ private static class Context + implements CacheExpiryListener { /** Physical JDBC connection */ private Connection physicalConn = null; /** Proxied JDBC connection which overrides close behavior */ private Connection proxyConn = null; + + /** Prepared statement cache which closes statements as they expire from the cache */ + private ExpiryCache psCache = null; + + /** + * Manage prepared statements which expire from the cache, by closing them. + * + * @param event + * Event which contains the prepared statement(s) which have expired from the cache. + */ + @Override + public void cacheExpired(CacheExpiryEvent event) + { + for (UnclosablePreparedStatement ps : event.getExpiredEntries().values()) + { + try + { + if (!ps.isClosed()) + { + if (ps.isCheckedOut()) + { + ps.setCloseOnCheckIn(true); + } + else + { + ps.forceClose(); + } + } + } + catch (SQLException exc) + { + // eat it + } + } + } + } + } + + /** + * Prepared statement cache key. + */ + private static class PSKey + { + /** SQL string, including placeholders */ + private final String sql; + + /** Statement result set type */ + private final int resultSetType; + + /** Statement result set concurrency */ + private final int resultSetConcurrency; + + /** Cached hash code */ + private final int hashCode; + + /** + * Constructor. + * + * @param sql + * SQL string, including placeholders + * @param resultSetType + * Statement result set type + * @param resultSetConcurrency + * Statement result set concurrency + */ + private PSKey(String sql, int resultSetType, int resultSetConcurrency) + { + this.sql = sql; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + this.hashCode = computeHash(); + } + + /** + * Compute and return a well distributed hash code. + * + * @return Computed hash code. + */ + private int computeHash() + { + int result = 17; + result = 37 * result + sql.hashCode(); + result = 37 * result + resultSetType; + result = 37 * result + resultSetConcurrency; + + return result; + } + + /** + * Return a hash code for this object. + * + * @return Hash code. + */ + @Override + public int hashCode() + { + return hashCode; + } + + /** + * Test this object for equality (equivalence) with the given object. + * + * @param o + * Object to test. + * + * @return {@code true} if this object is the same instance as the given object, or if it is + * considered equivalent to the given object, else {@code false}. + */ + @Override + public boolean equals(Object o) + { + if (this == o) + { + // same instance + return true; + } + + if (!(o instanceof PSKey)) + { + // impossible to be equivalent + return false; + } + + PSKey that = (PSKey) o; + + return this.resultSetType == that.resultSetType && + this.resultSetConcurrency == that.resultSetConcurrency && + this.sql.equals(that.sql); } } } === added file 'src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java' --- src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java 2021-01-13 21:04:41 +0000 @@ -0,0 +1,373 @@ +/* +** Module : UnclosablePreparedStatement.java +** Abstract : Provides a prepared statement proxy suitable for caching. +** +** Copyright (c) 2020, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------Description---------------------------------- +** 001 AIL 20201020 Created initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ +package com.goldencode.p2j.persist.orm; + +import java.io.*; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.*; +import java.util.Calendar; +import java.util.logging.*; + +import com.goldencode.p2j.util.LogHelper; + +/** + * Prepared statement proxy used for caching statements at connection level. + */ +public class UnclosablePreparedStatement implements PreparedStatement +{ + /** The proxied prepared statement */ + private PreparedStatement target; + + /** Flag to indicate if this statement is checked-out */ + private boolean checkedOut; + + /** Flag to indicate if this statement should close itself on check-in */ + private boolean closeOnCheckIn; + + /** Logger */ + private static final Logger LOG = LogHelper.getLogger(UnclosablePreparedStatement.class.getName()); + + /** + * Default constructor + */ + public UnclosablePreparedStatement(PreparedStatement target) + { + this(target, true); + } + + /** + * Constructor. + * + * @param target + * Represents the prepared statement instance which should be proxied + * @param closeOnCheckIn + * Flag to indicate id the statement should be closed when checked-in + */ + public UnclosablePreparedStatement(PreparedStatement target, boolean closeOnCheckIn) + { + this.target = target; + this.closeOnCheckIn = closeOnCheckIn; + this.checkedOut = false; + } + + /** + * Set the checkOut flag on true. + */ + public void checkOut() + { + if (!isCheckedOut()) + { + this.checkedOut = true; + } + else + { + throw new IllegalStateException("Can't check out a statement which was already checked-out."); + } + } + + /** + * Set the checkOut flag on false. If the closeOnCheckIn is set, force close this statement. + */ + public void checkIn() + { + if (isCheckedOut()) + { + this.checkedOut = false; + + try + { + if (isCloseOnCheckIn()) + { + forceClose(); + } + else + { + // make sure that the result set will be closed + // cached statements will also automatically cache the result-set + if (this.target.getResultSet() != null) + { + this.target.getResultSet().close(); + } + + // make sure that the generated keys will be closed + // cached statements will also automatically cache the generated-keys + if (this.target.getGeneratedKeys() != null) + { + this.target.getGeneratedKeys().close(); + } + } + + } + catch (SQLException e) + { + if (LOG.isLoggable(Level.WARNING)) + { + LOG.log(Level.WARNING, "Can't close the freshly checked-in statement: " + target.toString()); + } + } + } + else + { + throw new IllegalStateException("Can't check in a statement which wasn't yet checked-out."); + } + } + + /** + * Check if this statement is checked out + * + * @return true if this statement is checked out + */ + public boolean isCheckedOut() + { + return this.checkedOut; + } + + /** + * Check if this statement should close when checked-in + * + * @return true if this statement should close when checked-in + */ + public boolean isCloseOnCheckIn() + { + return this.closeOnCheckIn; + } + + /** + * Set the closeOnCheckIn flag. + * + * @param closeOnCheckIn + * The new value for the closeOnCheckIn flag. + */ + public void setCloseOnCheckIn(boolean closeOnCheckIn) + { + this.closeOnCheckIn = closeOnCheckIn; + } + + /** + * Close the proxy. This will flag that this statement is inactive and can be checked-in the pool. + */ + public void close() throws SQLException + { + // closing a statement will eventually mean the return of it in the pool - so check-in + checkIn(); + } + + /** + * Permanently close the prepared statement as this will be removed from the pool. + * @throws SQLException + */ + public void forceClose() throws SQLException + { + // close such prepared statements only in force mode + // this also closes the attached result set if any + if (!isCheckedOut()) + { + this.target.close(); + } + else + { + throw new IllegalStateException("Can't close a statement which is still checked-out."); + } + } + + public ResultSet executeQuery(String sql) throws SQLException { return this.target.executeQuery(sql); } + public int executeUpdate(String sql) throws SQLException { return this.target.executeUpdate(sql); } + public int getMaxFieldSize() throws SQLException { return this.target.getMaxFieldSize(); } + public void setMaxFieldSize(int max) throws SQLException { this.target.setMaxFieldSize(max); } + public int getMaxRows() throws SQLException { return this.target.getMaxRows(); } + public void setMaxRows(int max) throws SQLException { this.target.setMaxRows(max); } + public void setEscapeProcessing(boolean enable) throws SQLException { this.target.setEscapeProcessing(enable); } + public int getQueryTimeout() throws SQLException { return this.target.getQueryTimeout(); } + public void setQueryTimeout(int seconds) throws SQLException { this.target.setQueryTimeout(seconds); } + public void cancel() throws SQLException { this.target.cancel(); } + public SQLWarning getWarnings() throws SQLException { return this.target.getWarnings(); } + public void clearWarnings() throws SQLException { this.target.clearWarnings(); } + public void setCursorName(String name) throws SQLException { this.target.setCursorName(name); } + public boolean execute(String sql) throws SQLException { return this.target.execute(sql); } + public ResultSet getResultSet() throws SQLException { return this.target.getResultSet(); } + public int getUpdateCount() throws SQLException { return this.target.getUpdateCount(); } + public boolean getMoreResults() throws SQLException { return this.target.getMoreResults(); } + public void setFetchDirection(int direction) throws SQLException { this.target.setFetchDirection(direction); } + public int getFetchDirection() throws SQLException { return this.target.getFetchDirection(); } + public void setFetchSize(int rows) throws SQLException { this.target.setFetchSize(rows); } + public int getFetchSize() throws SQLException { return this.target.getFetchSize(); } + public int getResultSetConcurrency() throws SQLException { return this.target.getResultSetConcurrency(); } + public int getResultSetType() throws SQLException { return this.target.getResultSetType(); } + public void addBatch(String sql) throws SQLException { this.target.addBatch(sql); } + public void clearBatch() throws SQLException { this.target.clearBatch(); } + public int[] executeBatch() throws SQLException { return this.target.executeBatch(); } + public Connection getConnection() throws SQLException { return this.target.getConnection(); } + public boolean getMoreResults(int current) throws SQLException { return this.target.getMoreResults(current); } + public ResultSet getGeneratedKeys() throws SQLException { return this.target.getGeneratedKeys(); } + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return this.target.executeUpdate(sql, autoGeneratedKeys); } + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return this.target.executeUpdate(sql, columnIndexes); } + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + return this.target.executeUpdate(sql, columnNames); } + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return this.target.execute(sql, autoGeneratedKeys); } + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return this.target.execute(sql, columnIndexes); } + public boolean execute(String sql, String[] columnNames) throws SQLException { + return this.target.execute(sql, columnNames); } + public int getResultSetHoldability() throws SQLException { return this.target.getResultSetHoldability(); } + public boolean isClosed() throws SQLException { return this.target.isClosed(); } + public void setPoolable(boolean poolable) throws SQLException { this.target.setPoolable(poolable); } + public boolean isPoolable() throws SQLException { return this.target.isPoolable(); } + public void closeOnCompletion() throws SQLException { this.target.isCloseOnCompletion(); } + public boolean isCloseOnCompletion() throws SQLException { return this.target.isCloseOnCompletion(); } + public T unwrap(Class iface) throws SQLException { return this.target.unwrap(iface); } + public boolean isWrapperFor(Class iface) throws SQLException { return this.target.isWrapperFor(iface); } + public ResultSet executeQuery() throws SQLException { return this.target.executeQuery(); } + public int executeUpdate() throws SQLException { return this.target.executeUpdate(); } + public void setNull(int parameterIndex, int sqlType) throws SQLException { + this.target.setNull(parameterIndex, sqlType); } + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + this.target.setBoolean(parameterIndex, x); } + public void setByte(int parameterIndex, byte x) throws SQLException { this.target.setByte(parameterIndex, x); } + public void setShort(int parameterIndex, short x) throws SQLException { this.target.setShort(parameterIndex, x); } + public void setInt(int parameterIndex, int x) throws SQLException { this.target.setInt(parameterIndex, x); } + public void setLong(int parameterIndex, long x) throws SQLException { this.target.setLong(parameterIndex, x); } + public void setFloat(int parameterIndex, float x) throws SQLException { this.target.setFloat(parameterIndex, x); } + public void setDouble(int parameterIndex, double x) throws SQLException { + this.target.setDouble(parameterIndex, x); } + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + this.target.setBigDecimal(parameterIndex, x); } + public void setString(int parameterIndex, String x) throws SQLException { + this.target.setString(parameterIndex, x); } + public void setBytes(int parameterIndex, byte[] x) throws SQLException { this.target.setBytes(parameterIndex, x); } + public void setDate(int parameterIndex, Date x) throws SQLException { this.target.setDate(parameterIndex, x); } + public void setTime(int parameterIndex, Time x) throws SQLException { this.target.setTime(parameterIndex, x); } + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + this.target.setTimestamp(parameterIndex, x); } + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + this.target.setAsciiStream(parameterIndex, x, length); } + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + this.target.setUnicodeStream(parameterIndex, x, length);} + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + this.target.setBinaryStream(parameterIndex, x, length); } + public void clearParameters() throws SQLException { this.target.clearParameters(); } + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + this.target.setObject(parameterIndex, x, targetSqlType);} + public void setObject(int parameterIndex, Object x) throws SQLException { + this.target.setObject(parameterIndex, x); } + public boolean execute() throws SQLException { return this.target.execute(); } + public void addBatch() throws SQLException { this.target.addBatch(); } + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + this.target.setCharacterStream(parameterIndex, reader, length); } + public void setRef(int parameterIndex, Ref x) throws SQLException { this.target.setRef(parameterIndex, x); } + public void setBlob(int parameterIndex, Blob x) throws SQLException { this.target.setBlob(parameterIndex, x); } + public void setClob(int parameterIndex, Clob x) throws SQLException { this.target.setClob(parameterIndex, x); } + public void setArray(int parameterIndex, Array x) throws SQLException { this.target.setArray(parameterIndex, x); } + public ResultSetMetaData getMetaData() throws SQLException { return this.target.getMetaData(); } + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + this.target.setDate(parameterIndex, x, cal); } + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + this.target.setTime(parameterIndex, x, cal); } + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + this.target.setTimestamp(parameterIndex, x, cal); } + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + this.target.setNull(parameterIndex, sqlType, typeName); } + public void setURL(int parameterIndex, URL x) throws SQLException { this.target.setURL(parameterIndex, x); } + public ParameterMetaData getParameterMetaData() throws SQLException { return this.target.getParameterMetaData(); } + public void setRowId(int parameterIndex, RowId x) throws SQLException { this.target.setRowId(parameterIndex, x); } + public void setNString(int parameterIndex, String value) throws SQLException { + this.target.setNString(parameterIndex, value); } + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + this.target.setNCharacterStream(parameterIndex, value, length); } + public void setNClob(int parameterIndex, NClob value) throws SQLException { + this.target.setNClob(parameterIndex, value); } + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + this.target.setClob(parameterIndex, reader, length); } + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + this.target.setBlob(parameterIndex, inputStream, length); } + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + this.target.setNClob(parameterIndex, reader, length); } + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + this.target.setSQLXML(parameterIndex, xmlObject); } + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + this.target.setObject(parameterIndex, x, targetSqlType, scaleOrLength); } + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + this.target.setAsciiStream(parameterIndex, x, length); } + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + this.target.setBinaryStream(parameterIndex, x, length); } + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + this.target.setCharacterStream(parameterIndex, reader, length); } + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + this.target.setAsciiStream(parameterIndex, x); } + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + this.target.setBinaryStream(parameterIndex, x); } + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + this.target.setCharacterStream(parameterIndex, reader); } + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + this.target.setNCharacterStream(parameterIndex, value); } + public void setClob(int parameterIndex, Reader reader) throws SQLException { + this.target.setClob(parameterIndex, reader); } + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + this.target.setBlob(parameterIndex, inputStream); } + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + this.target.setNClob(parameterIndex, reader); } + +} === modified file 'src/com/goldencode/p2j/persist/orm/Validation.java' --- src/com/goldencode/p2j/persist/orm/Validation.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/persist/orm/Validation.java 2021-01-26 21:32:14 +0000 @@ -2,7 +2,7 @@ ** Module : Validation.java ** Abstract : Database record validation and persistence operation. ** -** Copyright (c) 2004-2020, Golden Code Development Corporation. +** Copyright (c) 2004-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20200207 First revision. Validates non-nullable fields and unique constraints. @@ -13,6 +13,9 @@ ** 20200929 Performance: UNION ALL multiple unique index validation SQL statements together to ** reduce number of queries prepared/sent. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** AIL 20201020 Closed unclosed prepared statement. +** ECF 20201201 Fixed FFC invalidation for flush update case when record was validated previously. +** 20210126 Ensure all unvalidated unique indices are validated at flush time. */ /* @@ -298,7 +301,7 @@ Object[] diff = singleProp ? dmo.getActiveUpdateDiffs() : null; // validate fully updated unique indices, if any - BitSet valUnique = dmo.getUnvalidatedIndices(offset); + BitSet valUnique = dmo.getUnvalidatedIndices(offset, flush); if (!valUnique.isEmpty()) { // validate unique indices (and flush, if flush was requested) @@ -490,8 +493,8 @@ { // invalidate only affected indexes on update int dirtyOffset = dmo.getActiveOffset(); - BitSet dirtyUnique = dmo.getDirtyIndices(dirtyOffset, true, true); - BitSet dirtyNonunique = dmo.getDirtyIndices(dirtyOffset, false, true); + BitSet dirtyUnique = dmo.getDirtyIndices(dirtyOffset, true, false); + BitSet dirtyNonunique = dmo.getDirtyIndices(dirtyOffset, false, false); ffc.invalidate(recMeta.getDmoMeta().getId(), multiplex, dirtyUnique, dirtyNonunique); } @@ -1009,9 +1012,8 @@ query = uniqueSQLs[check.nextSetBit(0)]; } - try + try (PreparedStatement ps = session.getConnection().prepareStatement(query)) { - PreparedStatement ps = session.getConnection().prepareStatement(query); int psOffset = 0; for (int k = check.nextSetBit(0); k >= 0; k = check.nextSetBit(k + 1)) === modified file 'src/com/goldencode/p2j/persist/orm/types/TypeManager.java' --- src/com/goldencode/p2j/persist/orm/types/TypeManager.java 2020-07-02 19:29:20 +0000 +++ src/com/goldencode/p2j/persist/orm/types/TypeManager.java 2020-12-15 19:03:37 +0000 @@ -7,6 +7,7 @@ ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 OM 20191101 Created initial version. Basic BDT support in both direction. ** CA 20200622 Small performance improvement - use IdentityHashMap if the key is java.lang.Class. +** 002 CA 20201214 Fixed setByteArrayParameter when the value is null - the type is VARBINARY. */ /* @@ -539,7 +540,7 @@ { if (val == null) { - stmt.setNull(index, Types.BLOB); + stmt.setNull(index, Types.VARBINARY); } else { === modified file 'src/com/goldencode/p2j/persist/pl/Contains.java' --- src/com/goldencode/p2j/persist/pl/Contains.java 2020-01-20 11:21:22 +0000 +++ src/com/goldencode/p2j/persist/pl/Contains.java 2020-12-23 16:58:23 +0000 @@ -8,6 +8,7 @@ ** 001 ECF 20180830 Created initial version. ** 002 ECF 20190309 Silently tolerate, but do not match on an empty string expression. ** 003 CA 20190821 Allow a specified set of characters to be used as word delimiters. +** 004 IAS 20201113 Re-worked using Reverse Polish Notation and LogicalExpressionConverter */ /* @@ -69,6 +70,7 @@ import java.util.*; import java.util.concurrent.*; import com.goldencode.p2j.util.*; +import com.goldencode.p2j.util.LogicalExpressionConverter.*; /** * Simplified implementation of the CONTAINS operator for lightweight use. Evaluates a data @@ -118,29 +120,8 @@ */ public final class Contains { - /** Constant for no match expression token type */ - private static final int NONE = -1; - - /** Constant for full word match expression token type */ - private static final int WORD = 1; - - /** Constant for word prefix match expression token type */ - private static final int PREFIX = 2; - - /** Constant for logical AND operator match expression token type */ - private static final int AND = 3; - - /** Constant for logical OR operator match expression token type */ - private static final int OR = 4; - - /** Constant for left parenthesis match expression token type */ - private static final int LPARENS = 5; - - /** Constant for right parenthesis match expression token type */ - private static final int RPARENS = 6; - /** Cache of match expressions to parsed ASTs */ - private static final Map cache = new ConcurrentHashMap<>(); + private static final Map> cache = new ConcurrentHashMap<>(); /** Single character word delimiter for data strings */ private final Set delim = new HashSet<>(); @@ -148,8 +129,8 @@ /** Should matches be checked case-sensitively? */ private final boolean caseSensitive; - /** Parsed match expression AST, used to evaluate matches with string data */ - private final Ast exprAst; + /** Parsed match expression RPN, used to evaluate matches with string data */ + private final List exprRPN; /** * Constructor. @@ -188,83 +169,10 @@ this.delim.add(String.valueOf(ch)); } this.caseSensitive = caseSensitive; - Ast ast = cache.get(expr); - if (ast == null) - { - ast = parseExpression(expr); - cache.putIfAbsent(expr, ast); - } - this.exprAst = ast; - } - - /** - * Look up the string representation of a match expression token type. - * - * @param type - * Token type. - * - * @return Symbolic token name. - */ - private static String lookupTokenName(int type) - { - String name = null; - - switch (type) - { - case WORD: - name = "WORD"; - break; - case PREFIX: - name = "PREFIX"; - break; - case AND: - name = "AND"; - break; - case OR: - name = "OR"; - break; - case LPARENS: - name = "LPARENS"; - break; - case RPARENS: - name = "RPARENS"; - break; - case NONE: - name = "NONE"; - break; - default: - name = ""; - break; - } - - return name; - } - - /** - * Parse the given match expression into an AST which can be used to evaluate matches. - * - * @param expr - * Match expression which uses CONTAINS operator syntax. - * - * @return AST representing the match expression. - * - * @throws ErrorConditionException - * if there is a syntax or an I/O error parsing the match expression. - */ - private Ast parseExpression(String expr) - { - try - { - Reader reader = new StringReader(expr); - Lexer lexer = new Lexer(reader, delim, caseSensitive); - Parser parser = new Parser(lexer); - - return parser.parse(); - } - catch (IOException exc) - { - throw new ErrorConditionException(exc); - } + this.exprRPN = expr == null ? Collections.emptyList() : cache.computeIfAbsent( + expr, + exp -> new LogicalExpressionConverter(exp, caseSensitive, delim).toRPN(false) + ); } /** @@ -282,105 +190,67 @@ */ public boolean evaluate(String data) { - boolean result = false; - + Stack stack = new Stack<>(); + Set words = words(data); try { - Reader reader = new StringReader(data); - int size = exprAst.getSize(); - boolean[] results = new boolean[size]; - + for (EToken token: exprRPN) + { + if (token instanceof Term) + { + stack.push(((Term)token).evaluate(words)); + } + else if (token instanceof Op) + { + switch ((Op)token) + { + case NOT: + stack.push(!stack.pop()); + break; + case AND: + stack.push(stack.pop() & stack.pop()); + break; + case OR: + stack.push(stack.pop() | stack.pop()); + break; + } + } + } + return stack.pop(); + } + catch (EmptyStackException e) + { + throw new ErrorConditionException(e); + } + } + /** + * Split a string to a set of words + * @param data + * string to be parsed + * @return + * words contained in a string + */ + public Set words(String data) + { + Set words = new HashSet<>(); + try (Reader reader = new StringReader(data)) + { String word; - while (!result && (word = nextWord(reader)) != null) + while ((word = nextWord(reader)) != null) { - result = evaluate(exprAst, word, results); + if (!caseSensitive) + { + word = word.toUpperCase(); + } + words.add(word); } - } - catch (IOException exc) - { - throw new ErrorConditionException(exc); - } - - return result; - } - - /** - * Evaluate the match expression for a single expression node and return the result. - *

- * Positive results of individual AST node matches for each invocation of this method are - * preserved in the given array of results, which is indexed by the zero-based, contiguously - * assigned IDs of the nodes. This array is first consulted before evaluating a node match. - * If the result for that node already is set to {@code true} from a previous invocation for - * an earlier word in the data stream, the evaluation of that node is bypassed and the {@code - * true} result is returned. - * - * @param node - * AST node currently being visited for evaluation. - * @param word - * Current word from the data stream to be matched. - * @param results - * Stateful array of results from previous invocations of this method against earlier - * words in the data stream. If the current node of the AST match expression - * evaluates to {@code true}, the corresponding element in the array is updated - * accordingly. - * - * @return {@code true} if the match operation for the current node succeeded, else {@code - * false}. - */ - private boolean evaluate(Ast node, String word, boolean[] results) - { - int i = node.id; - - if (results[i]) - { - return true; - } - - boolean match = false; - - int type = node.getType(); - switch (type) - { - case WORD: - // full word match - match = word.equals(node.getText()); - break; - case PREFIX: - // partial (starts with) word match - match = word.startsWith(node.getText()); - break; - case NONE: - // empty expression is silently ignored, but does not produce a match - break; - case AND: - // logical AND of two child nodes - { - boolean b1 = evaluate(node.down, word, results); - boolean b2 = evaluate(node.down.right, word, results); - match = b1 && b2; - } - break; - case OR: - // logical OR of two child nodes - { - boolean b1 = evaluate(node.down, word, results); - boolean b2 = evaluate(node.down.right, word, results); - match = b1 || b2; - } - break; - default: - break; - } - - if (match) - { - // update corresponding element in results array - results[i] = true; - } - - return match; - } - + return words; + } + catch (IOException | EmptyStackException e) + { + throw new ErrorConditionException(e); + } + } /** * Get the next word from the match string, using this object's delimiter. *

@@ -405,7 +275,7 @@ { char c = (char) next; - if (delim.contains(String.valueOf(c))) + if (Character.isWhitespace(c) || delim.contains(String.valueOf(c))) { reader.mark(1); @@ -443,665 +313,6 @@ return word; } - /** - * Lexer which builds a token stream from the match expression. Used by the parser to build - * an AST. Since the expressions syntax is very simple, lookahead is limited to one (the - * current) token. - */ - private static class Lexer - { - /** String reader stream */ - private final Reader reader; - - /** Should matches be checked case-sensitively? */ - private final boolean caseSensitive; - - /** The word-delimiter characters. */ - private final Set delim; - - /** Token most recently read from the expression */ - private Token current = null; - - /** Flag indicating if the last char(s) read was a word delimiter. */ - private boolean hasDelim = false; - - /** - * Constructor. - * - * @param reader - * Reader stream on the match expression text. - * @param caseSensitive - * {@code true} to perform word match comparisons case-sensitively, {@code false} - * to ignore case. - */ - private Lexer(Reader reader, Set delim, boolean caseSensitive) - { - this.reader = reader; - this.delim = delim; - this.caseSensitive = caseSensitive; - } - - /** - * Report the type of the most recently read token, or {@code NONE} if no more tokens are - * available. - * - * @return Token type of current token, or {@code NONE}. - * - * @throws IOException - * if there is an I/O error reading from the stream. - */ - int type() - throws IOException - { - if (current == null) - { - readNextToken(); - } - - return (current != null ? current.type : NONE); - } - - /** - * Return the most recently read token, or {@code null} if no more tokens are available. - * - * @return Current token or {@code null}. - * - * @throws IOException - * if there is an I/O error reading from the stream. - */ - Token token() - throws IOException - { - if (current == null) - { - readNextToken(); - } - - return current; - } - - /** - * Mark the current token as having been consumed. - */ - void consume() - { - current = null; - } - - /** - * Read the next token from the match expression and store it as the lexer's current token. - * - * @throws IOException - * if there is an I/O error reading from the stream. - */ - private void readNextToken() - throws IOException - { - if (hasDelim) - { - current = new Token(AND, ""); - hasDelim = false; - - // consume all delimiter chars - reader.mark(1); - int next; - while ((next = reader.read()) >= 0) - { - char c = (char) next; - if (!delim.contains(String.valueOf(c))) - { - break; - } - - reader.mark(1); - } - - // reset just before the last non-delimiter char was read - reader.reset(); - return; - } - - StringBuilder buf = new StringBuilder(); - int type = NONE; - int next; - - read: - while ((next = reader.read()) >= 0) - { - char c = (char) next; - - if (delim.contains(String.valueOf(c))) - { - // consume all delimiter chars - hasDelim = true; - break; - } - - switch (c) - { - case '*': - // wildcard which marks the current word as a prefix to be matched - if (buf.length() == 0) - { - // wildcard cannot start a token - wildcardError(); - - break read; - } - type = PREFIX; - reader.mark(0); - break; - - case '&': - case '|': - case '^': - case '!': - case '(': - case ')': - if (buf.length() > 0) - { - // if a token is not finished being processed when we encounter this - // character, we reset the reader to just before this character and - // process that token first; the next time through, we'll process this - // character in the else block below - reader.reset(); - } - else - { - switch (c) - { - case '&': - type = AND; - break; - case '|': - case '^': - case '!': - type = OR; - break; - case '(': - type = LPARENS; - break; - case ')': - type = RPARENS; - break; - } - buf.append(c); - } - break read; - - default: - - // if we've already encountered the wildcard indicator and we're now reading - // more text to match, we have a syntax error - if (type == PREFIX) - { - wildcardError(); - - break read; - } - - // all other content is interpreted as text to match - buf.append(c); - - // mark the stream in case we need to reset upon hitting an operator or - // parenthesis character before we have processed the current word/prefix - reader.mark(0); - break; - } - } - - Token tok = null; - - // if there is content in the buffer or a type has been assigned, create a token - if (type != NONE || buf.length() > 0) - { - String text = buf.toString(); - if (!caseSensitive) - { - text = text.toUpperCase(); - } - - if (type == NONE) - { - type = WORD; - } - - tok = new Token(type, text); - } - else if (type == NONE) - { - if (hasDelim) - { - hasDelim = false; - readNextToken(); - return; - } - - tok = new Token(type, ""); - } - - current = tok; - } - - /** - * Record or throw an error about incorrect wildcard syntax. - * - * @throws ErrorConditionException - * always. - */ - private void wildcardError() - { - String msg = "QBW syntax error - An asterisk (*) is allowed only at the end of a word"; - - throw new ErrorConditionException(4686, msg); - } - } - - /** - * Contains match expression parser. Creates an AST that can be used to evaluate the - * expression against a data string. - */ - private static class Parser - { - /** Contains match expression lexer */ - private final Lexer lexer; - - /** Stack for intermediate AST nodes created during parsing */ - private final Deque stack = new LinkedList<>(); - - /** - * Constructor. - * - * @param lexer - * Contains match expression lexer. - */ - Parser(Lexer lexer) - { - this.lexer = lexer; - } - - /** - * Parse the contains match expression. - * - * @return An AST representing the parsed expression. - * - * @throws ErrorConditionException - * if there is any error recognizing expected tokens. - * @throws IOException - * if there is any error lexing tokens. - */ - Ast parse() - throws IOException - { - orExpr(); - - // at this point, we've parsed a complete expression; anything left is garbage - if (lexer.type() != NONE) - { - throw new ErrorConditionException("Unexpected token: " + lexer.token()); - } - - Ast ast = popAst(); - - ast.initialize(0); - - return ast; - } - - /** - * Parse the first (lowest) level of expression precedence: the binary OR operator. - * - * @throws ErrorConditionException - * if there is any error recognizing expected tokens. - * @throws IOException - * if there is any error lexing tokens. - */ - private void orExpr() - throws IOException - { - if (lexer.type() != OR) - { - andExpr(); - } - - do - { - if (lexer.type() == OR) - { - Token operator = lexer.token(); - Ast c1 = popAst(); - lexer.consume(); - andExpr(); - Ast c2 = popAst(); - - Ast ast = new Ast(operator); - ast.addChild(c1); - ast.addChild(c2); - stack.push(ast); - } - else - { - break; - } - } while (true); - } - - /** - * Parse the second level of expression precedence: the binary AND operator. - * - * @throws ErrorConditionException - * if there is any error recognizing expected tokens. - * @throws IOException - * if there is any error lexing tokens. - */ - private void andExpr() - throws IOException - { - if (lexer.type() != AND) - { - primaryExpr(); - } - - do - { - if (lexer.type() == AND) - { - Token operator = lexer.token(); - Ast c1 = popAst(); - lexer.consume(); - primaryExpr(); - Ast c2 = popAst(); - - Ast ast = new Ast(operator); - ast.addChild(c1); - ast.addChild(c2); - stack.push(ast); - } - else - { - break; - } - } while (true); - } - - /** - * Parse the third (highest) level of expression precedence: the primary match word or - * prefix, or a sub-expression in parentheses. Parenthesis tokens are not included as - * nodes in the AST, but they may change its structure. - * - * @throws ErrorConditionException - * if there is any error recognizing expected tokens. - * @throws IOException - * if there is any error lexing tokens. - */ - private void primaryExpr() - throws IOException - { - switch (lexer.type()) - { - case NONE: - if (!stack.isEmpty()) - { - // TODO: proper Progress error - throw new ErrorConditionException("Expression ended prematurely"); - } - // fall-through intentional; we want to create a NONE token, if it is the only - // content of the expression (i.e., the full expression was empty string) - case WORD: - case PREFIX: - Ast ast = new Ast(lexer.token()); - stack.push(ast); - lexer.consume(); - break; - case LPARENS: - // uncomment the lines below to add LPARENS node to AST - // Ast lpAst = new Ast(lexer.token()); - // stack.push(lpAst); - lexer.consume(); - orExpr(); - if (lexer.type() != RPARENS) - { - // TODO: proper Progress error - throw new ErrorConditionException("Expected closing parenthesis"); - } - lexer.consume(); - // uncomment the lines below to add LPARENS node to AST - // Ast child = popAst(); - // lpAst.addChild(child); - // stack.push(lpAst); - break; - case RPARENS: - // TODO: proper Progress error - String msg = "Unexpected closing parenthesis " + lexer.token(); - throw new ErrorConditionException(msg); - default: - throw new ErrorConditionException("Unrecognized token: " + lexer.token()); - } - } - - /** - * Pop the top-most AST from the working stack. - * - * @return The top-most AST node. - * - * @throws ErrorConditionException - * if there was no AST node on the working stack. - */ - private Ast popAst() - { - try - { - return stack.pop(); - } - catch (NoSuchElementException exc) - { - throw new ErrorConditionException("Internal parsing error"); - } - } - } - - /** - * Match expression token. - */ - private static class Token - { - /** Token type */ - private final int type; - - /** Token text */ - private final String text; - - /** - * Constructor. - * - * @param type - * Token type. - * @param text - * Token text. - */ - private Token(int type, String text) - { - this.type = type; - this.text = text; - } - - /** - * Return a string representation of this token. - * - * @return String representation for debug purposes. - */ - @Override - public String toString() - { - return text + " <" + lookupTokenName(type) + ">"; - } - } - - /** - * A minimalist AST implementation for match expressions. - */ - private static class Ast - { - /** Token associated with this node */ - private final Token token; - - /** Unique identifier of this node */ - private int id = -1; - - /** First child node */ - private Ast down = null; - - /** Next sibling node */ - private Ast right = null; - - /** Number of nodes including and under this node */ - private int size = -1; - - /** - * Constructor for a single AST expression node. - * - * @param token - * Expression token. - */ - private Ast(Token token) - { - this.token = token; - } - - /** - * Return a string representation of this node. - * - * @return See above. - */ - @Override - public String toString() - { - return token.toString() + " id=" + id + " size=" + size; - } - - /** - * Get the number of nodes including and under this node. - * - * @return See above. - */ - int getSize() - { - return size; - } - - /** - * Get the token type associated with this node. - * - * @return Token type. - */ - int getType() - { - return token.type; - } - - /** - * Get the token text associated with this node. - * - * @return See above. - */ - String getText() - { - return token.text; - } - - /** - * Add a child to this node. - * - * @param child - * Child node to add. - */ - void addChild(Ast child) - { - if (down == null) - { - down = child; - - return; - } - - Ast next = down; - while (next.right != null) - { - next = next.right; - } - - next.right = child; - } - - /** - * Initialize the {@code id} and {@code size} attributes of this and all descendant nodes. - * Walks the tree in a depth-first enumeration. - * - * @param next - * ID to assign to this node. - * - * @return Next unique ID to be assigned to this node's first child or next sibling. - */ - int initialize(int next) - { - id = next++; - - if (down != null) - { - next = down.initialize(next); - } - - size = next - id; - - if (right != null) - { - next = right.initialize(next); - } - - return next; - } - - /** - * Print a string representation of this tree to the given print stream for debug - * purposes. - * - * @param out - * Target print stream. - */ - void print(PrintStream out) - { - print(out, 0); - } - - /** - * Print a properly indented string representation of this single node to the given print - * stream. - * - * @param out - * Target print stream. - * @param level - * Descendant level of this node in relation to the root node. - */ - private void print(PrintStream out, int level) - { - for (int i = 0; i < level; i++) - { - out.append(" "); - } - - out.println(this); - - if (down != null) - { - down.print(out, level + 1); - } - - if (right != null) - { - right.print(out, level); - } - } - } /** * Simple test harness for command line testing. @@ -1115,16 +326,19 @@ try { String expr = args[0]; - String data = args[1]; - boolean caseSens = args.length > 2 && !"0".equals(args[2]); - Contains contains = new Contains(expr, caseSens, ' '); - contains.exprAst.print(System.out); - boolean match = contains.evaluate(data); - System.out.println("Result = " + match); +// boolean caseSens = args.length > 2 && !"0".equals(args[2]); + Contains contains = new Contains(expr, true); + System.out.println(contains.exprRPN); + for (int i = 1; i < args.length; i++) + { + String data = args[i]; + boolean match = contains.evaluate(data); + System.out.printf("[%s]: %s\n", data, match); + } } - catch (Exception exc) + catch (Exception e) { - exc.printStackTrace(); + e.printStackTrace(); } } } === modified file 'src/com/goldencode/p2j/persist/pl/Functions.java' --- src/com/goldencode/p2j/persist/pl/Functions.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/pl/Functions.java 2020-12-23 16:58:23 +0000 @@ -91,6 +91,7 @@ ** 039 AIL 20191111 Change initialization of ErrorHandler's static context. ** 040 ECF 20200504 Added missing alias for entryIn(Long, String). ** 041 OM 20200212 Unknown value fixes. +** 042 IAS 20201121 Added 'words' functions. */ /* @@ -250,6 +251,46 @@ } /** + * Split a string to an array of words + * @param data + * string to be parsed + * @return + * words contained in a string + */ + @HQLFunction + public static String[] words(String data) + { + return words(data, false); + } + + /** + * Split a string to an array of words + * @param data + * string to be parsed + * @param toUpperCase + * convert words to uppercase + * @return + * words contained in a string + */ + @HQLFunction + public static String[] words(String data, boolean toUpperCase) + { + if (data == null) + { + return new String[0]; + } + try + { + return new Contains(null, !toUpperCase, delims()).words(data.trim()).toArray(new String[0]); + } + catch (RuntimeException exc) + { + ErrorHandler.handleError(exc); + return null; + } + } + + /** * Test whether a data string contains the word(s) indicated by the given expression. * optionally taking case into account. */ @@ -268,41 +309,47 @@ { try { - String explicitDelims = System.getProperty(WORD_DELIMITERS); - if (explicitDelims == null) - { - explicitDelims = System.getenv(WORD_DELIMITERS); - } - char[] delims = null; - if (explicitDelims == null) - { - // WARNING: this list is not exhaustive. 4GL has a kind of 'if it can't be part of - // a word, then it must be a delimiter' approach, as there is no explicit list of - // delimiters in the proword.0 (or proword.def) file - // TODO: enhance this to all the proper default delimiter chars. - delims = DEFAULT_WORD_DELIMITERS; - } - else - { - String[] toks = explicitDelims.split(","); - delims = new char[toks.length]; - for (int i = 0; i < toks.length; i++) - { - delims[i] = (char) Integer.parseInt(toks[i], 16); - } - } - - Contains contains = new Contains(expr, caseSensitive, delims); - - return contains.evaluate(data); + return new Contains(expr, caseSensitive, delims()).evaluate(data); + } catch (RuntimeException exc) { ErrorHandler.handleError(exc); - return null; } } + + /** + * Get word delimiters + * @return word delimiters. + */ + private static char[] delims() + { + String explicitDelims = System.getProperty(WORD_DELIMITERS); + if (explicitDelims == null) + { + explicitDelims = System.getenv(WORD_DELIMITERS); + } + char[] delims = null; + if (explicitDelims == null) + { + // WARNING: this list is not exhaustive. 4GL has a kind of 'if it can't be part of + // a word, then it must be a delimiter' approach, as there is no explicit list of + // delimiters in the proword.0 (or proword.def) file + // TODO: enhance this to all the proper default delimiter chars. + delims = DEFAULT_WORD_DELIMITERS; + } + else + { + String[] toks = explicitDelims.split(","); + delims = new char[toks.length]; + for (int i = 0; i < toks.length; i++) + { + delims[i] = (char) Integer.parseInt(toks[i], 16); + } + } + return delims; + } /** * Returns the character value of a numeric expression which must be @@ -3511,4 +3558,12 @@ { return i == null ? null : i.longValue(); } + + public static void main(String... args) throws Exception + { + for (String s: words(args[0], true)) + { + System.out.println(s); + } + } } === modified file 'src/com/goldencode/p2j/persist/serial/ExtentTracker.java' --- src/com/goldencode/p2j/persist/serial/ExtentTracker.java 2019-01-28 07:24:19 +0000 +++ src/com/goldencode/p2j/persist/serial/ExtentTracker.java 2020-11-20 18:40:59 +0000 @@ -2,11 +2,13 @@ ** Module : ExtentTracker.java ** Abstract : Helper object to track extents during temp-table data import. ** -** Copyright (c) 2017-2019, Golden Code Development Corporation. +** Copyright (c) 2017-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20190123 Extracted from XmlImport. +** 002 OM 20201120 Small fix in constructor. */ + /* ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU Affero General Public License as @@ -86,7 +88,7 @@ for (TempTableSchema.Column column : schema.columns()) { Integer extent = column.getExtent(); - if (extent != null) + if (extent != null && extent > 0) { String name = column.getName(); ExtentTracker.Tracker tracker = new Tracker(extent); @@ -173,4 +175,4 @@ current = 0; } } -} \ No newline at end of file +} === modified file 'src/com/goldencode/p2j/persist/serial/JsonExport.java' --- src/com/goldencode/p2j/persist/serial/JsonExport.java 2021-01-30 11:24:40 +0000 +++ src/com/goldencode/p2j/persist/serial/JsonExport.java 2021-01-31 10:14:52 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2019-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20190107 Created initial version. ** 002 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source. ** 003 OM 20190626 Added serialization support for DATASETs. @@ -16,6 +16,8 @@ ** 005 ECF 20200906 New ORM implementation. ** 006 CA 20200914 Added blob, raw, rowid and handle support. ** 007 CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. +** 008 OM 20201120 Improved compatibility with P4GL. +** 009 ME 20210130 Added serializeTempTable method into JsonArray. */ /* @@ -156,7 +158,12 @@ boolean beforeImage) throws PersistenceException { - try (OutputStream out = target.getOutputStream()) + if (!target.configureSupportedTargets(TargetData.TD_WRITE_JSON, LegacyResource.DATASET + " widget")) + { + return; + } + + try (OutputStream out = target.getStream()) { StreamJsonSerializer sjs = new StreamJsonSerializer(out, encoding, pretty); exportDataSet(dataSet, noInit, noOuter, beforeImage, sjs); @@ -323,7 +330,12 @@ logical omitOuterObject) throws PersistenceException { - try (OutputStream out = target.getOutputStream()) + if (!target.configureSupportedTargets(TargetData.TD_WRITE_JSON, LegacyResource.TEMP_TABLE + " widget")) + { + return; + } + + try (OutputStream out = target.getStream()) { boolean pretty = formatted != null && !formatted.isUnknown() && formatted.booleanValue(); StreamJsonSerializer sjs = new StreamJsonSerializer(out, encoding, pretty); @@ -380,37 +392,64 @@ } /** - * Serializes TEMP-TABLE to json. - * - * @param buffer - * Temp-table buffer in which data is stored. - * @param omitInitialValues - * {@code True} to omit data for fields whose values match their initial values. - * @param json - * This is the target for the deserialized json data. - * - * @throws PersistenceException - * On persistence issues. + * Exports a single record in JSON format. + * + * @param bufferImpl + * The buffer holding the record to be exported. + * @param targetData + * The output struture. + * @param formatted + * Use {@code true} to have a pretty output. Otherwise the output is generated on a single line. + * @param jsonEncoding + * The encoding to be used. + * @param omitInitial + * If {@code true} the initial/default values are skipped. + * @param omitOuter + * Omit outer values. + * + * @return The buffer holding the record to be exported. */ - public void readTempBuffer(TemporaryBuffer buffer, - logical omitInitialValues, - JsonStructureCallback json) - throws PersistenceException + public boolean exportRecord(BufferImpl bufferImpl, + TargetData targetData, + boolean formatted, + JsonEncoding jsonEncoding, + boolean omitInitial, + boolean omitOuter) { - boolean noInit = !omitInitialValues.isUnknown() && omitInitialValues.getValue(); - - try - { - serializeTempTable(json, buffer, noInit); - } - catch (IOException exc) - { - throw new PersistenceException("Error exporting JSON data", exc); - } - catch (RuntimeException exc) - { - throw new PersistenceException(exc.getMessage(), exc); - } + if (!targetData.configureSupportedTargets(TargetData.TD_WRITE_JSON, LegacyResource.BUFFER + " widget")) + { + return false; + } + + TemporaryBuffer tempBuffer = (TemporaryBuffer) bufferImpl.buffer(); + XmlTempTableSchema schema = new XmlTempTableSchema(tempBuffer); +// if (checkSupportedTypes(schema)) +// { +// return false; +// } + + try (OutputStream out = targetData.getStream()) + { + StreamJsonSerializer json = new StreamJsonSerializer(out, jsonEncoding, formatted); + if (!omitOuter) + { + json.startObject(); + json.fieldName(bufferImpl.doGetName()); + } + writeRecord(json, tempBuffer, tempBuffer.getCurrentRecord(), schema, bufferImpl.doGetName(), + true, false, false, false, omitInitial); + if (!omitOuter) + { + json.endObject(); + } + json.closeStream(); + } + catch (IOException e) + { + return false; + } + + return true; } /** @@ -436,34 +475,42 @@ * @param dsFlags * {@code true} to add the internal flags specific to DataSet TempTable records. * @param dsBeforeImage - * {@code true} to write the before-image if available. - * + * {@code true} to write the before-image if available. + * + * @return {@code true} if everything went fine, and {@code false} if a non-fatal error was encountered + * * @throws IOException * If exceptions occur during the serialization. */ - private void serializeTempTable(JsonStructureCallback json, - DataRelation relation, - TemporaryBuffer buffer, - String overrideBeforeName, - boolean noInit, - boolean omitOuter, - boolean hiddenFields, - boolean dsFlags, - boolean dsBeforeImage) + private boolean serializeTempTable(JsonStructureCallback json, + DataRelation relation, + TemporaryBuffer buffer, + String overrideBeforeName, + boolean noInit, + boolean omitOuter, + boolean hiddenFields, + boolean dsFlags, + boolean dsBeforeImage) throws IOException { TempTableSchema schema = new TempTableSchema(buffer); boolean isBefore = (overrideBeforeName != null); + TempTable tempTable = (TempTable) buffer.tableHandle().get(); + String serializeName = tempTable.getSerializeName().toJavaType(); String tableName = isBefore - ? overrideBeforeName - : !schema.getSerializeOptions().getSerializeName().isEmpty() - ? schema.getSerializeOptions().getSerializeName() + ? overrideBeforeName + : serializeName != null && !serializeName.isEmpty() + ? serializeName : schema.getTableName(); - + +// if (checkSupportedTypes(schema)) +// { +// return false; +// } + if (relation == null) { - // reads records in order of primary index, if available, else in ascending primary key - // order + // reads records in order of primary index, if available, else in ascending primary key order if (!omitOuter) { json.startObject(); @@ -473,7 +520,7 @@ json.fieldName(tableName); } json.startArray(); - + buffer.readAllRows( (dmo) -> writeRecord(json, buffer, @@ -485,14 +532,14 @@ dsFlags && isBefore, dsBeforeImage, noInit)); - + json.endArray(); if (!omitOuter) { json.endObject(); } - return; + return true; } // build a query depending on the relation, and add each record from the relation @@ -527,12 +574,12 @@ { json.endArray(); } + + return true; } /** - * Actual implementation of a table serialization. This is called for both for TEMP-TABLEs - * (once) and for DATASETs (one time for each member buffer, and supplementary for any eventual - * before-buffer) + * Actual implementation of a table serialization into a JsonArray (only rows). * * @param json * The target of the deserialized json data. @@ -540,21 +587,23 @@ * A buffer of the temp-table to be serialized. * @param noInit * Skip the initial values. - * + * @throws IOException * If exceptions occur during the serialization. */ - private void serializeTempTable(JsonStructureCallback json, - TemporaryBuffer buffer, - boolean noInit) + public boolean serializeTempTable(JsonStructureCallback json, + TemporaryBuffer buffer, + boolean noInit) throws IOException { TempTableSchema schema = new TempTableSchema(buffer); - String tableName = !schema.getSerializeOptions().getSerializeName().isEmpty() - ? schema.getSerializeOptions().getSerializeName() + TempTable tempTable = (TempTable) buffer.tableHandle().get(); + String serializeName = tempTable.getSerializeName().toJavaType(); + String tableName = serializeName != null && !serializeName.isEmpty() + ? serializeName : schema.getTableName(); - - buffer.readAllRows( + + buffer.readAllRows( (dmo) -> writeRecord(json, buffer, dmo, @@ -565,8 +614,8 @@ false, false, noInit)); - - + + return true; } /** @@ -606,8 +655,6 @@ return; } - // TODO: noInit !!! - Integer rowState = dmo._rowState(); if (rowState == null) { @@ -615,19 +662,8 @@ } if (skipCreateFields && rowState == Buffer.ROW_CREATED) { - // NOTE: it seems like the CREATED records are NOT serialized at all for - // BEFORE TEMP-TABLEs, so these value should not reach past this point, so those - // records are cut here. - return; - } - - String unsupportedType = Util.checkSupportedTypes(schema); - if (unsupportedType != null) - { - // NOTE: this test is executed for each serialized row! Error 15391 is printed multiple - // times in message line, one for each record. - ErrorManager.recordOrShowError(15391, unsupportedType); - // Unsupported data type for JSON serialization: . + // NOTE: it seems like the CREATED records are NOT serialized at all for BEFORE TEMP-TABLEs, so these + // values should not reach past this point, so the records are cut here return; } @@ -665,9 +701,26 @@ } Integer extent = column.getExtent(); - - if (extent != null) + if (extent != null && extent > 0) { + if (noInit) + { + boolean untouched = true; + for (int i = 0; i < extent; i++) + { + if (column.isChanged((BaseDataType) column.getGetter().invoke(dmo, i))) + { + untouched = false; + break; // drop all other tests from this extent + } + } + + if (untouched) + { + continue; // skip to next column + } + } + json.fieldName(name); json.startArray(); @@ -680,6 +733,13 @@ } else { + if (noInit) + { + if (!column.isChanged((BaseDataType) column.getGetter().invoke(dmo))) + { + continue; // skip to next column + } + } writeDatum(json, column, dmo, null); } } @@ -688,7 +748,7 @@ { BufferImpl buf = (BufferImpl) buffer.getDMOProxy(); - DataSet ds = (DataSet) buf.dataSet().getResource(); + DataSet ds = buf._dataSet(); if (ds != null) { // position the buffer on this DMO @@ -721,6 +781,26 @@ } /** + * Checks supported data types. The evaluation is delegated to {@code Util.checkSupportedTypes()}. + * The method returns {@code true} and prints an error messages is unsupported types are detected. + * + * @return {@code true} if a type problem was discovered. + */ + private boolean checkSupportedTypes(TempTableSchema schema) + { + String unsupportedType = Util.checkSupportedTypes(schema); + if (unsupportedType != null) + { + // NOTE: this test is executed for each serialized row! Error 15391 is printed multiple + // times in message line, one for each record. + ErrorManager.recordOrShowError(15391, unsupportedType); + // Unsupported data type for JSON serialization: . + return true; + } + return false; + } + + /** * Write a single data value to the JSON generator's output stream, according to the * instructions stored in the temp-table schema. * @@ -733,6 +813,8 @@ * @param index * Zero-based index of an extent field element. Should be {@code null} for a scalar * field. + * + * @return {@code false} on errors. The error was already reported through {@code ErrorManager}. * * @throws IOException * if the JSON generator cannot write data. @@ -743,7 +825,7 @@ * @throws IllegalAccessException * if there is an access/security problem. */ - private void writeDatum(JsonStructureCallback json, + private boolean writeDatum(JsonStructureCallback json, TempTableSchema.Column column, Record dmo, Integer index) @@ -764,7 +846,13 @@ if (((BaseDataType) datum).isUnknown()) { json.nullValue(); - return; + return true; + } + else if (!Util.checkSupportedType(column)) + { + ErrorManager.recordOrThrowError(17586, Util.getLegacyType(column.getType())); + // Unsupported data type for JSON serialization: . + return false; } // lookup JSON writer function by data type; we cache lambdas to minimize use of @@ -852,7 +940,9 @@ } else { - throw new RuntimeException("Field type " + type + " is not supportef for JSON!"); + ErrorManager.recordOrShowError(15391, Util.getLegacyType(type)); + // Unsupported data type for JSON serialization: . + return false; } // TODO: other data types ? @@ -865,12 +955,13 @@ if (fn == null) { - UnimplementedFeature.missing("JSON export for type: " + type); - - return; + ErrorManager.recordOrShowError(15391, Util.getLegacyType(type)); + // Unsupported data type for JSON serialization: . + return false; } fn.accept(datum); + return true; } /** === modified file 'src/com/goldencode/p2j/persist/serial/JsonImport.java' --- src/com/goldencode/p2j/persist/serial/JsonImport.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/persist/serial/JsonImport.java 2021-01-27 01:54:24 +0000 @@ -2,14 +2,15 @@ ** Module : JsonImport.java ** Abstract : Read JSON content into a temp-table. ** -** Copyright (c) 2017-2019, Golden Code Development Corporation. +** Copyright (c) 2017-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20190123 Created initial version. ** 002 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source. ** 003 OM 20190818 Improve reading and validation support. ** 004 ECF 20200906 New ORM implementation. ** CA 20200906 Batch error fix. +** 005 OM 20210108 Improve reading compatibility with ABL JSON files. */ /* @@ -71,8 +72,10 @@ import java.util.*; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.goldencode.p2j.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.lock.*; +import com.goldencode.p2j.persist.orm.*; import com.goldencode.p2j.util.*; import static com.fasterxml.jackson.core.JsonToken.*; import static com.goldencode.p2j.persist.serial.SerializeOptions.*; @@ -99,6 +102,9 @@ /** Helper which tracks current index values for extent fields */ private ExtentTracker extentTracker; + /** The primary index which drives the duplicate resolution, in case the read mode is REPLACE. */ + private P2JIndex primaryIndex = null; + /** JSON parser */ private JsonParser parser = null; @@ -108,8 +114,10 @@ /** Current dataset, if one is being processed. */ private DataSet ds = null; + /** {@code true} when reading a before-table image. */ private boolean readingBefore = false; + /** Associates the {@code prod:id} to their record's {@code rowid}.*/ private Map peerMapping = null; /** @@ -142,6 +150,11 @@ public boolean readTable(TemporaryBuffer buffer) throws PersistenceException { + if (!source.configureSupportedSources(SourceData.SD_READ_JSON, LegacyResource.TEMP_TABLE + " widget")) + { + return false; + } + this.schema = new TempTableSchema(buffer); this.proxy = (Buffer) buffer.getDMOProxy(); this.extentTracker = new ExtentTracker(schema); @@ -155,7 +168,7 @@ JsonFactory factory = mapper.getFactory(); String tableName = schema.getTableName(); - try (InputStream stream = source.getInputStream(); + try (InputStream stream = source.getStream(); JsonParser parser = factory.createParser(stream)) { this.parser = parser; @@ -214,10 +227,15 @@ public boolean readDataset(DataSet ds) throws PersistenceException { + if (!source.configureSupportedSources(SourceData.SD_READ_JSON, LegacyResource.DATASET + " widget")) + { + return false; + } + ObjectMapper mapper = new ObjectMapper(); JsonFactory factory = mapper.getFactory(); - try (InputStream stream = source.getInputStream(); + try (InputStream stream = source.getStream(); JsonParser parser = factory.createParser(stream)) { this.ds = ds; @@ -232,18 +250,14 @@ } String name = parser.nextFieldName(); - if (!ds.name().toStringMessage().equals(name)) + String dsName = ds.name().toStringMessage(); + if (!dsName.equals(name)) { - // TODO: proper message and error reporting - String msg = "Mismatched table names in JSON and temp-table schema"; -// String msg = "Temp-Table name '" + -// tableName + -// "' in namespace '' not found in JSON Document."; -// ErrorManager.displayError(13514, msg); - -// throw new PersistenceException(msg); + ErrorManager.recordOrShowError(15375, name, dsName); + // Dataset name '' in JSON does not match ''. return false; } + if (!readDataSetContent()) { return false; @@ -253,6 +267,11 @@ return false; } } + catch (FileNotFoundException exc) + { + ErrorManager.recordOrShowError(293, source.getFileName()); + return false; + } catch (IOException exc) { throw new PersistenceException(exc); @@ -339,6 +358,19 @@ this.schema = new TempTableSchema(buffer.buffer()); this.proxy = (Buffer) buffer.buffer().getDMOProxy(); this.extentTracker = new ExtentTracker(schema); + this.primaryIndex = null; + + if (readMode == Read.REPLACE && !readingBefore) + { + // the REPLACE mode needs primary unique index declared for the after table + this.primaryIndex = buffer.buffer().getDmoInfo().getPrimaryIndex(true); + if (this.primaryIndex == null) + { + ErrorManager.recordOrShowError(13063, schema.getTableName()); + // REPLACE mode requires a unique primary index in the target table . + return false; + } + } readTableContent(); } @@ -419,6 +451,7 @@ String name = null; boolean processNested = false; boolean batchError = true; + RecordBuffer recBuffer = ((BufferImpl) proxy).buffer(); try { RecordBuffer.startBatch(true); @@ -439,7 +472,7 @@ if (tok == START_ARRAY) { // this is a nested DataSet table: the current row is assumed complete, - // allow batch to end and call recursivelly + // allow batch to end and call recursively processNested = true; batchError = false; break; @@ -525,7 +558,63 @@ finally { extentTracker.reset(); + + if (readMode == Read.MERGE || readMode == Read.REPLACE) + { + boolean validated = false; + try + { + validated = recBuffer.validate(false); + } + catch (ValidationException e) + { + // ignore, no 'expected' exception will be thrown + } + + // was there an unique conflict with this record? + if (!validated) + { + if (readMode == Read.MERGE) + { + // merge mode: ignore it, just skip to next record + proxy.release(); // TODO: not enough + } + else + { + // replace mode: eliminate competition, then try again on clean ground + try + { + OrmUtils.dropUniqueIndexConflicts(recBuffer.buffer(), + recBuffer.buffer().getPersistence(), + recBuffer.buffer().getMultiplexID()); + } + catch (PersistenceException e) + { + throw new ErrorConditionException( + "FILL: Failed to make room for new record in REPLACE mode", e); + } + // now there should not be any collisions and the new record should be flushed + } + } + } + + ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper(); + boolean wm = eh.isWarningMode(); + if (!wm) + { + // this is mostly for APPEND mode, the other ones will not encounter issues here + eh.setWarningMode(true); + } RecordBuffer.endBatch(batchError); + if (!wm) + { + eh.setWarningMode(false); + } + // if (ErrorManager.error().toJavaType()) + if (ErrorManager.isPendingError()) + { + return false; + } } if (processNested) === added file 'src/com/goldencode/p2j/persist/serial/Serializator.java' --- src/com/goldencode/p2j/persist/serial/Serializator.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/persist/serial/Serializator.java 2021-01-21 22:49:45 +0000 @@ -0,0 +1,133 @@ +/* +** Module : Serializator.java +** Abstract : An interface for storing common constants from SourceData and TargetData. +** +** Copyright (c) 2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- -------------------------------Description-------------------------------- +** 001 OM 20210120 Initial version. +*/ + +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.persist.serial; + +import java.io.*; + +/** + * An interface for storing common constants from Source and TargetData. It also declares a single method, + * {@code getStream()} which will have to be implemented by returning the appropriate stream to be processed + * by the XML/JSON processors. + */ +public interface Serializator +extends Closeable +{ + /** Bit constant: the method supports FILE read/write operations. */ + public static final int SER_FILE = 0x0001; + + /** Bit constant: the method supports STREAM read/write operations. */ + public static final int SER_STREAM = 0x0002; + + /** Bit constant: the method supports STREAM_HANDLE read/write operations. */ + public static final int SER_STREAM_HANDLE = 0x0004; + + /** Bit constant: the method supports MEMPTR read/write operations. */ + public static final int SER_MEMPTR = 0x0008; + + /** Bit constant: the method supports HANDLE read/write operations. */ + public static final int SER_HANDLE = 0x0010; + + /** Bit constant: the method supports LONGCHAR read/write operations. */ + public static final int SER_LONGCHAR = 0x0020; + + /** Bit constant: the method supports JSON_OBJECT read/write operations. */ + public static final int SER_JSON_OBJECT = 0x0040; + + /** Bit constant: the method supports JSON_ARRAY read/write operations. */ + public static final int SER_JSON_ARRAY = 0x0080; + + /** String constant: case insensitive (in uppercase) for serialization into/from a FILE. */ + public static final String TYPE_FILE = "FILE"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a STREAM variable. */ + public static final String TYPE_STREAM = "STREAM"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a STREAM-HANDLE. */ + public static final String TYPE_STREAM_HANDLE = "STREAM-HANDLE"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a MEMPTR variable. */ + public static final String TYPE_MEMPTR = "MEMPTR"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a HANDLE. */ + public static final String TYPE_HANDLE = "HANDLE"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a LONGCHAR variable. */ + public static final String TYPE_LONGCHAR = "LONGCHAR"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a JsonObject variable. */ + public static final String TYPE_JSON_OBJECT = "JSONOBJECT"; + + /** String constant: case insensitive (in uppercase) for serialization into/from a JsonArray variable. */ + public static final String TYPE_JSON_ARRAY = "JSONARRAY"; + + /** + * The method to be implemented by the sources. + * + * @return The stream as declared in the constructor, if any. Otherwise {@code null}. + * + * @throws IOException + * In case of errors while opening the stream. + */ + public Closeable getStream() throws IOException; +} === modified file 'src/com/goldencode/p2j/persist/serial/SerializeOptions.java' --- src/com/goldencode/p2j/persist/serial/SerializeOptions.java 2019-12-03 21:59:03 +0000 +++ src/com/goldencode/p2j/persist/serial/SerializeOptions.java 2021-01-27 01:54:24 +0000 @@ -2,13 +2,14 @@ ** Module : SerializeOptions.java ** Abstract : Options related to serialized temp-table data. ** -** Copyright (c) 2017-2019, Golden Code Development Corporation. +** Copyright (c) 2017-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 ECF 20171016 Created initial version. ** 002 OM 20181114 Made serializeName mutable. ** 003 HC 20190301 Extended XmlNodeType with other possible values. ** 004 CA 20191203 Made XmlNodeType class public. +** 005 OM 20010121 Allow the field members be mutable. */ /* @@ -98,19 +99,19 @@ } /** Should this field be omitted from serialized output? */ - private final boolean serializeHidden; + private boolean serializeHidden; /** Name of field in serialized output (overrides field name). */ private String serializeName; /** XML schema data type of field. */ - private final String xmlDataType; + private String xmlDataType; /** Name of element or attribute representing field in XML output. */ - private final String xmlNodeName; + private String xmlNodeName; /** Node type of field in XML output. */ - private final XmlNodeType xmlNodeType; + private XmlNodeType xmlNodeType; /** * Constructor. @@ -142,8 +143,7 @@ /** * Get the value of the serialize-hidden flag. * - * @return {@code true} if the associated field is not included in serialization, else - * {@code false}. + * @return {@code true} if the associated field is not included in serialization, else {@code false}. */ public boolean isSerializeHidden() { @@ -151,6 +151,17 @@ } /** + * Sets the value of the serialize-hidden flag. + * + * @param serializeHidden + * {@code true} if the associated field is not included in serialization, else {@code false}. + */ + public void setSerializeHidden(boolean serializeHidden) + { + this.serializeHidden = serializeHidden; + } + + /** * Get the name of the field in serialized output (overrides field name in table). * * @return Serialize name. @@ -182,8 +193,18 @@ } /** - * Get the node name of the field in XML output (overrides serialize-name and field name in - * table). + * Sets the XML schema data type of the associated field. + * + * @param xmlDataType + * The new XML schema data type. + */ + public void setXmlDataType(String xmlDataType) + { + this.xmlDataType = xmlDataType; + } + + /** + * Get the node name of the field in XML output (overrides serialize-name and field name in table). * * @return XML node name. */ @@ -193,6 +214,17 @@ } /** + * Sets the node name of the field in XML output (overrides serialize-name and field name in table). + * + * @param xmlNodeName + * The new XML node name. + */ + public void setXmlNodeName(String xmlNodeName) + { + this.xmlNodeName = xmlNodeName; + } + + /** * Get the node type (element or attribute) in XML output. * * @return XML node type. @@ -201,4 +233,15 @@ { return xmlNodeType; } + + /** + * Sets the node type (element or attribute) in XML output. + * + * @param xmlNodeType + * The new XML node type. + */ + public void setXmlNodeType(XmlNodeType xmlNodeType) + { + this.xmlNodeType = xmlNodeType; + } } === modified file 'src/com/goldencode/p2j/persist/serial/TempTableSchema.java' --- src/com/goldencode/p2j/persist/serial/TempTableSchema.java 2020-09-06 23:15:41 +0000 +++ src/com/goldencode/p2j/persist/serial/TempTableSchema.java 2021-01-29 00:53:41 +0000 @@ -2,9 +2,9 @@ ** Module : TempTableSchema.java ** Abstract : Temp-table schema used for serialization, based on internal structure of the table. ** -** Copyright (c) 2017-2020, Golden Code Development Corporation. +** Copyright (c) 2017-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20171020 Created initial version. ** 002 CA 20180511 Added XML-NODE-NAME table option support. ** 003 ECF 20190120 Refactored out XML-specific features into separate XmlTempTableSchema class. @@ -12,6 +12,9 @@ ** 005 OM 20190823 Added help and format field option. ** CA 20190826 Column lookup must be case-insensitive (as 4GL is case-insensitive). ** 006 ECF 20200906 New ORM implementation. +** 007 OM 20201029 Added case sensitive column support. Added detection for changed fields. +** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local +** dmoMeta.legacyTable. */ /* @@ -96,11 +99,11 @@ /** List of column schema objects whose data is to be serialized as XML elements */ private final List elementColumns; - /** The serialize options defined at the temp-table. */ - protected final TableSerializeOptions serializeOptions; - /** Flags the serialized {@code BEFORE-BUFFERS}. */ - private final boolean beforeImage; + private final boolean beforeImage; + + /** Flags the no-undoable temp-tables {@code NO-UNDO} option. */ + private final boolean noUndo; /** * Constructor which gathers table and field data from a temp-table buffer. @@ -111,19 +114,19 @@ public TempTableSchema(RecordBuffer buffer) { BufferImpl buf = (BufferImpl) buffer.getDMOProxy(); + RecordBuffer recBuf = buf.buffer(); // the table name is used for serialization, regardless of the individual buffer name - this.name = TableMapper.getLegacyName(buf); - this.serializeOptions = TableMapper.getSerializeOptions(buf); + this.name = recBuf.getDmoInfo().legacyTable; Class dmoIface = buffer.getDMOInterface(); - Map extentMap = PropertyHelper.extentsByProperty(dmoIface); Map getterMap = PropertyHelper.allGettersByProperty(dmoIface); Map setterMap = PropertyHelper.allSettersByProperty(dmoIface); TempTable tt = buffer.getParentTable(); List fields = TableMapper.getAllLegacyFieldInfo(tt); Iterator iter = fields.iterator(); + this.noUndo = !tt.canUndo().toJavaType(); // indexes: int k = 0; @@ -148,18 +151,23 @@ TableMapper.LegacyFieldInfo info = iter.next(); // add column to schema if it should be serialized - if (!info.getSerializeOptions().isSerializeHidden()) - { - Column col = new Column(i + 1, getterMap, setterMap, extentMap, info); - columns.put(col.getName().toLowerCase(), col); - } + if (info.getSerializeOptions().isSerializeHidden()) + { + continue; + } + + Column col = new Column(i + 1, getterMap, setterMap, info, recBuf.getDMOInterface()); + if (columns.containsKey(col.getName().toLowerCase())) + { + continue; // exclude extent column duplicates + } + + columns.put(col.getName().toLowerCase(), col); } this.beforeImage = buf.isBeforeBuffer(); - this.attributeColumns = filterColumns(columns.values(), - SerializeOptions.XmlNodeType.ATTRIBUTE); - this.elementColumns = filterColumns(columns.values(), - SerializeOptions.XmlNodeType.ELEMENT); + this.attributeColumns = filterColumns(columns.values(), SerializeOptions.XmlNodeType.ATTRIBUTE); + this.elementColumns = filterColumns(columns.values(), SerializeOptions.XmlNodeType.ELEMENT); } /** @@ -214,13 +222,13 @@ } /** - * Get the serialize options for this temp-table. + * Checks whether the temp-table was defined as NO-UNDO. * - * @return The {@link #serializeOptions}. + * @return {@code true} if this is a NO-UNDO temp-table. */ - public TableSerializeOptions getSerializeOptions() + public boolean isNoUndo() { - return serializeOptions; + return noUndo; } /** @@ -323,27 +331,36 @@ private final String format; /** The string representation of the INIT option for this column. */ - private final String defaultVal; + private final String initial; + + /** The initial value as an instance of the appropriate type of the INIT option for this column. */ + private final BaseDataType initialValue; + + /** Flags case-sensitive character fields. */ + private final boolean caseSensitive; + + /** The precision for decimal fields. */ + private final int decimals; /** * Constructor. - * + * * @param order * Order of column in serialization output. * @param getterMap * Map of java property names to getter methods. * @param setterMap * Map of java property names to setter methods. - * @param extentMap - * Map of java property names to field extent values. * @param field * Field info object. + * @param dmoInterface + * The DMO interface used to extract EXTENT accessors. */ Column(int order, Map getterMap, Map setterMap, - Map extentMap, - TableMapper.LegacyFieldInfo field) + TableMapper.LegacyFieldInfo field, + Class dmoInterface) { SerializeOptions opts = field.getSerializeOptions(); @@ -367,16 +384,78 @@ // name used in serialized output is the legacy name of the field, unless overridden // by the SERIALIZE-NAME attribute, unless overridden by the XML-NODE-NAME attribute // TODO: possibly needs rework to differentiate between XML and JSON serialization - this.name = (nodeName != null ? nodeName : serName != null ? serName : fieldName); + this.name = (nodeName != null) ? nodeName : (serName != null ? serName : fieldName); String javaName = field.getJavaName(); - this.getter = getterMap.get(javaName); - this.setter = setterMap.get(javaName); - this.extent = extentMap.get(javaName); + this.extent = field.getExtent(); + if (extent == 0 || field.getOriginal().isEmpty()) + { + this.getter = getterMap.get(javaName); + this.setter = setterMap.get(javaName); + } + else + { + String originalName = field.getOriginal(); + originalName = Character.toUpperCase(originalName.charAt(0)) + originalName.substring(1); + Method[] allMethods = dmoInterface.getDeclaredMethods(); + Method setterMeth = null; + Method getterMeth = null; + for (Method method : allMethods) + { + if (method.getName().equals("set" + originalName) && + method.getReturnType() == Void.TYPE && + method.getParameterCount() == 2 && + method.getParameterTypes()[0] == Integer.TYPE) + { + setterMeth = method; + } + else if (method.getParameterCount() == 1 && + method.getParameterTypes()[0] == Integer.TYPE && + (method.getName().equals("get" + originalName) || + method.getName().equals("is" + originalName))) + { + getterMeth = method; + } + if (setterMeth != null && getterMeth != null) + { + break; + } + } + this.setter = setterMeth; + this.getter = getterMeth; + } + this.type = (Class) getter.getReturnType(); - this.defaultVal = field.getInitial(); + this.initial = field.getInitial(); + this.initialValue = field.getInitialValue(); this.format = field.getFormat(); - this.help = field.getHelp(); + this.help = field.getHelp(); + this.caseSensitive = field.isCaseSensitive(); + this.decimals = field.getDecimals(); + } + + public Column(String fieldName, + boolean nillable, + SerializeOptions.XmlNodeType nodeType, + String name, + Class type, + String initial, BaseDataType initialValue) + { + this.fieldName = fieldName; + this.nillable = nillable; + this.extent = 0; + this.order = 0; + this.nodeType = nodeType; + this.name = name; + this.getter = null; + this.setter = null; + this.type = type; + this.help = null; + this.format = null; + this.initial = initial; + this.initialValue = initialValue; + this.caseSensitive = false; + this.decimals = 0; } /** @@ -476,7 +555,7 @@ */ public String getInitial() { - return defaultVal; + return initial; } /** @@ -509,5 +588,49 @@ { return fieldName.startsWith("__"); } + + /** + * Checks whether this is a case-sensitive character field. + * + * @return {@code true} only if this is a case-sensitive character field. + */ + public boolean isCaseSensitive() + { + return caseSensitive; + } + + /** + * Obtain the precision for decimal fields. + * + * @return the precision for decimal fields. + */ + public int getDecimals() + { + return decimals; + } + + /** + * Test whether a value is different than the initial value for this column. + * + * @param datum + * The value to be tested. + * + * @return {@code true} if the value is different. + */ + public boolean isChanged(BaseDataType datum) + { + boolean initUnknown = initialValue == null || initialValue.isUnknown(); + if (datum.isUnknown() && initUnknown) + { + return false; // both unknowns + } + + if (datum.isUnknown() != initUnknown) + { + return true; // only one of them is unknown + } + + return !datum.equals(initialValue); + } } } === modified file 'src/com/goldencode/p2j/persist/serial/Util.java' --- src/com/goldencode/p2j/persist/serial/Util.java 2020-09-27 18:16:32 +0000 +++ src/com/goldencode/p2j/persist/serial/Util.java 2021-01-27 01:54:24 +0000 @@ -10,6 +10,7 @@ ** 003 OM 20191014 Added testing utility support methods. ** 004 CA 20200914 Added blob/clob support. ** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. +** OM 20201120 Added missing datatypes. Made are immutable now. */ /* @@ -90,57 +91,66 @@ static final String STR_ROW_MODIFIED= "modified"; /** Mapping between supported ABL/FWD types and XSD type used for writing schema. */ - static final Map xsdMap = new IdentityHashMap<>(); + static final Map, String> xsdMap; /** The default values for each supported ABL/FWD types. */ - static final Map defaultValuesMap = new IdentityHashMap<>(); + static final Map, String> defaultValuesMap; /** Extended information for XSD types that coalesce multiple ABL/FWD types. */ - static final Map xsdProdataMap = new IdentityHashMap<>(); + static final Map, String> xsdProdataMap; /* Initialize the static final internal data. */ static { - // TODO: check [defaultValuesMap] values - defaultValuesMap.put(integer.class, "0"); - defaultValuesMap.put(int64.class, "0"); - defaultValuesMap.put(decimal.class, "0"); - defaultValuesMap.put(logical.class, "false"); - defaultValuesMap.put(date.class, "?"); - defaultValuesMap.put(datetime.class, "?"); - defaultValuesMap.put(datetimetz.class, "?"); - defaultValuesMap.put(raw.class, ""); - defaultValuesMap.put(rowid.class, "?"); - defaultValuesMap.put(handle.class, "?"); - defaultValuesMap.put(recid.class, "?"); - defaultValuesMap.put(character.class, ""); - - xsdMap.put(integer.class, "xsd:int"); - xsdMap.put(int64.class, "xsd:long"); - xsdMap.put(decimal.class, "xsd:decimal"); - xsdMap.put(logical.class, "xsd:boolean"); - xsdMap.put(date.class, "xsd:date"); - xsdMap.put(datetime.class, "xsd:dateTime"); - xsdMap.put(datetimetz.class, "xsd:dateTime"); - xsdMap.put(raw.class, "xsd:base64Binary"); - xsdMap.put(rowid.class, "xsd:base64Binary"); - xsdMap.put(handle.class, "xsd:long"); - xsdMap.put(recid.class, "xsd:long"); - xsdMap.put(character.class, "xsd:string"); - xsdMap.put(clob.class, "xsd:string"); - xsdMap.put(blob.class, "xsd:base64Binary"); - - xsdProdataMap.put(datetime.class, "prodata:dateTime"); - xsdProdataMap.put(rowid.class, "prodata:rowid"); - xsdProdataMap.put(handle.class, "prodata:handle"); - xsdProdataMap.put(recid.class, "prodata:recid"); - xsdProdataMap.put(blob.class, "prodata:blob"); - xsdProdataMap.put(clob.class, "prodata:clob"); + Map, String> mutableDefMap = new IdentityHashMap<>(); + mutableDefMap.put(blob.class, "?"); + mutableDefMap.put(character.class, ""); + mutableDefMap.put(clob.class, "?"); + mutableDefMap.put(comhandle.class, "?"); + mutableDefMap.put(date.class, "?"); + mutableDefMap.put(datetime.class, "?"); + mutableDefMap.put(datetimetz.class, "?"); + mutableDefMap.put(decimal.class, "0"); + mutableDefMap.put(int64.class, "0"); + mutableDefMap.put(integer.class, "0"); + mutableDefMap.put(logical.class, "false"); + mutableDefMap.put(raw.class, ""); + mutableDefMap.put(recid.class, "?"); + mutableDefMap.put(rowid.class, "?"); + mutableDefMap.put(handle.class, "?"); /*widget-handle*/ + defaultValuesMap = Collections.unmodifiableMap(mutableDefMap); + + Map, String> mutableTypeMap = new IdentityHashMap<>(); + mutableTypeMap.put(blob.class, "xsd:base64Binary"); + mutableTypeMap.put(character.class, "xsd:string"); + mutableTypeMap.put(clob.class, "xsd:string"); + mutableTypeMap.put(comhandle.class, "xsd:long"); + mutableTypeMap.put(date.class, "xsd:date"); + mutableTypeMap.put(datetime.class, "xsd:dateTime"); + mutableTypeMap.put(datetimetz.class, "xsd:dateTime"); + mutableTypeMap.put(decimal.class, "xsd:decimal"); + mutableTypeMap.put(int64.class, "xsd:long"); + mutableTypeMap.put(integer.class, "xsd:int"); + mutableTypeMap.put(logical.class, "xsd:boolean"); + mutableTypeMap.put(raw.class, "xsd:base64Binary"); + mutableTypeMap.put(recid.class, "xsd:long"); + mutableTypeMap.put(rowid.class, "xsd:base64Binary"); + mutableTypeMap.put(handle.class, "xsd:long"); /*widget-handle*/ + xsdMap = Collections.unmodifiableMap(mutableTypeMap); + + Map, String> mutableSubTypeMap = new IdentityHashMap<>(); + mutableSubTypeMap.put(blob.class, "prodata:blob"); + mutableSubTypeMap.put(clob.class, "prodata:clob"); + mutableSubTypeMap.put(comhandle.class, "prodata:comHandle"); + mutableSubTypeMap.put(datetime.class, "prodata:dateTime"); + mutableSubTypeMap.put(recid.class, "prodata:recid"); + mutableSubTypeMap.put(rowid.class, "prodata:rowid"); + mutableSubTypeMap.put(handle.class, "prodata:handle"); /*widget-handle*/ + xsdProdataMap = Collections.unmodifiableMap(mutableSubTypeMap); // longchar not supported // memptr not supported // class Progress.Lang.Object not supported at runtime - // comhandle not supported } /** @@ -217,6 +227,20 @@ } /** + * Checks whether a column of a TEMP-TABLE might not be supported by XML/JSON serialization. + * + * @param column + * The column of the TEMP-TABLE to be checked. + * + * @return {@code true} if the whose TEMP-TABLE is compatible to be XML serialized. + */ + static boolean checkSupportedType(TempTableSchema.Column column) + { + Class bdtClass = column.getType(); + return xsdMap.containsKey(bdtClass); + } + + /** * Resolve the legacy name based on the FWD BDT implementation class. * * @param bdtClass === modified file 'src/com/goldencode/p2j/persist/serial/XmlExport.java' --- src/com/goldencode/p2j/persist/serial/XmlExport.java 2020-09-14 09:17:21 +0000 +++ src/com/goldencode/p2j/persist/serial/XmlExport.java 2021-01-27 01:54:24 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2017-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20171016 Created initial version. ** 002 CA 20180511 Added XML-NODE-NAME table option support. ** 003 ECF 20190123 Replaced TempTableSchema with XmlTempTableSchema. @@ -16,6 +16,9 @@ ** 007 CA 20191119 Do not explicitly XML-escape the value, as the writer will do this on its own. ** 008 ECF 20200906 New ORM implementation. ** 009 CA 20200914 Fixed raw/blob field export. +** OM 20201120 Added SERIALIZE-ROW. Improved TEMP-TABLE attribute serialization and compatibility with +** the output of P4GL. +** OM 20201218 Fixed serialization of BEFORE-BUFFER specific attributes. */ /* @@ -75,10 +78,10 @@ import java.io.*; import java.lang.reflect.*; +import java.math.*; import java.util.*; import javax.xml.stream.*; import org.apache.commons.lang3.tuple.*; -import org.apache.commons.lang.*; import com.ctc.wstx.api.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.util.*; @@ -132,7 +135,33 @@ private boolean isSchemaOnly = false; /** The list of indexes, in order of their buffers. */ - private Map> indexes = new HashMap<>(); + private final Map> indexes = new HashMap<>(); + + /** The map with eventual error strings. */ + private final Map> prodsErrors = new LinkedHashMap<>(); + + /** The namespace prefix for the currently exported temp-table, if any. */ + private String ttNsPrefix = null; + + private static final TempTableSchema.Column ERROR_FLAG = + new TempTableSchema.Column(Buffer.ERROR_FLAG_FIELD, true, SerializeOptions.XmlNodeType.ELEMENT, + Buffer.__ERROR_FLAG__, integer.class, null, new integer()); + + private static final TempTableSchema.Column ORIGIN_ROWID = + new TempTableSchema.Column(Buffer.ORIGIN_ROWID_FIELD, true, SerializeOptions.XmlNodeType.ELEMENT, + Buffer.__ORIGIN_ROWID__, rowid.class, null, new rowid()); + + private static final TempTableSchema.Column ERROR_STRING = + new TempTableSchema.Column(Buffer.ERROR_STRING_FIELD, true, SerializeOptions.XmlNodeType.ELEMENT, + Buffer.__ERROR_STRING__, character.class, null, new character()); + + private static final TempTableSchema.Column AFTER_ROWID = + new TempTableSchema.Column(Buffer.AFTER_ROWID_FIELD, true, SerializeOptions.XmlNodeType.ELEMENT, + Buffer.__AFTER_ROWID__, rowid.class, null, new rowid()); + + private static final TempTableSchema.Column ROW_STATE = + new TempTableSchema.Column(Buffer.ROW_STATE_FIELD, true, SerializeOptions.XmlNodeType.ELEMENT, + Buffer.__ROW_STATE__, integer.class, null, new integer()); /** * Constructor which saves the internal data for writing a XML/XSD stream. @@ -183,6 +212,11 @@ boolean writeBeforeImage, boolean omitInitialValues) { + if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML, LegacyResource.DATASET + " widget")) + { + return false; + } + if (writeXmlSchema && schemaLocation != null && !schemaLocation.isEmpty()) { ErrorManager.recordOrShowError(13031); @@ -196,7 +230,7 @@ dsName1 = "ProDataSet"; } final String dsName = dsName1; - character nsUri = dataSet.getNamespaceURI(); + character nsUri = dataSet.namespaceURI(); String nsUriStr = (nsUri.isUnknown()) ? "" : nsUri.toStringMessage(); String nodeNameStr = dataSet.getXmlNodeNameInternal(); this.ds = dataSet; @@ -218,7 +252,7 @@ if (writeXmlSchema) { if (!writeDatasetSchemaImpl( - dataSet, minXmlSchema, !omitInitialValues, dsName, nsUriStr, nodeNameStr)) + dataSet, minXmlSchema, !omitInitialValues, dsName, nsUriStr, nodeNameStr)) { return false; } @@ -327,6 +361,32 @@ lineBreak(); } + if (!prodsErrors.isEmpty()) + { + indent(); + writer.writeStartElement("prods:errors"); + lineBreak(); + ++indentLevel; + + Set>> errors = prodsErrors.entrySet(); + for (Map.Entry> error : errors) + { + indent(); + writer.writeStartElement(error.getValue().getLeft()); + writer.writeAttribute("prods:id", error.getKey()); + writer.writeAttribute("prods:error", error.getValue().getRight()); + writer.writeEndElement(); + lineBreak(); + } + + --indentLevel; + indent(); + writer.writeEndElement(); // prods:errors + lineBreak(); + + prodsErrors.clear(); + } + if (writeBeforeImage) { --indentLevel; @@ -375,10 +435,17 @@ boolean writeBeforeImage, boolean omitInitialValues) { + if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML, LegacyResource.TEMP_TABLE + " widget")) + { + return false; + } + if (writeXmlSchema && schemaLocation != null && !schemaLocation.isEmpty()) { ErrorManager.recordOrShowError(13031); // Cannot have both schema-location and write-schema arguments set. + ErrorManager.recordOrShowError(4065, "WRITE-XML", "TEMP-TABLE widget"); + // **The attribute on the has invalid arguments. return false; } @@ -401,8 +468,23 @@ return write(() -> { String tableName = schema.getTableName(); - writer.writeStartElement(tableName); + TempTable tt = buffer.getParentTable(); + ttNsPrefix = tt.namespacePrefix().toJavaType(); + String nsUri = tt.namespaceURI().toJavaType(); + String nodeName = tt.getXmlNodeName().toJavaType(); + String xmlNodeName = ttNsPrefix == null || nodeName == null ? tableName : ttNsPrefix + ":" + nodeName; + + writer.writeStartElement(xmlNodeName); + if (ttNsPrefix != null) + { + writer.writeAttribute("xmlns:" + ttNsPrefix, nsUri); + } + else + { +// writer.writeAttribute("xmlns", ""); + } writer.writeAttribute(ATTR_NS_XSI, NS_URI); + if (schemaLocation != null && !schemaLocation.isEmpty()) { writer.writeAttribute("xsi:noNamespaceSchemaLocation", schemaLocation); @@ -420,7 +502,8 @@ } } - if (!serializeTempTable(null, buffer, tableName + "Row", null, + String rowName = xmlNodeName + "Row"; + if (!serializeTempTable(null, buffer, rowName, null, omitInitialValues, dmoProxy.isBeforeBuffer(), false, false)) { return false; @@ -453,8 +536,14 @@ */ public boolean writeDatasetSchema(DataSet dataSet, boolean minSchema, boolean noInitial) { + if (!target.configureSupportedTargets(TargetData.TD_WRITE_XMLSCHEMA, + LegacyResource.DATASET + " widget")) + { + return false; + } + String dsName = dataSet.name().toStringMessage(); - character nsUri = dataSet.getNamespaceURI(); + character nsUri = dataSet.namespaceURI(); String nsUriStr = (nsUri.isUnknown()) ? "" : nsUri.toStringMessage(); String nodeNameStr = dataSet.getXmlNodeNameInternal(); this.ds = dataSet; @@ -481,6 +570,12 @@ */ public boolean writeTableSchema(TemporaryBuffer buffer, boolean minSchema, boolean noInitial) { + if (!target.configureSupportedTargets(TargetData.TD_WRITE_XMLSCHEMA, + LegacyResource.TEMP_TABLE + " widget")) + { + return false; + } + this.isSchemaOnly = true; return write(() -> writeTableSchema( (BufferImpl) buffer.getDMOProxy(), @@ -490,6 +585,69 @@ } /** + * Writes a single record. + * + * @param bufferImpl + * The buffer which holds the record to be exported. + * @param omitInitial + * If {@code true} the unmodified fields (those that are equals to the declared initial value) + * are skipped in order to minimize the output. + * + * @return {@code true} if operation is successful and {@code false} if an error occurred. + */ + public boolean writeRecord(BufferImpl bufferImpl, boolean omitInitial) + { + if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML, LegacyResource.BUFFER + " widget")) + { + return false; + } + + TemporaryBuffer tempBuffer = (TemporaryBuffer) bufferImpl.buffer(); + XmlTempTableSchema schema = new XmlTempTableSchema(tempBuffer); + + String unsupportedType = Util.checkSupportedTypes(schema); + if (unsupportedType != null) + { + ErrorManager.recordOrShowError(13078, unsupportedType); + // Unsupported data type for XML Serialization: . + ErrorManager.recordOrShowError(13093); + // Write temp-table data failed for WRITE-XML. + return false; + } + + return write(() -> { + String tableName = schema.getTableName(); + writer.writeStartElement(tableName); + writer.writeAttribute(ATTR_NS_XSI, NS_URI); + lineBreak(); + ++indentLevel; + + try + { + TempRecord dmo = tempBuffer.getCurrentRecord(); + + // write all columns which must be serialized to attributes first, while the start element for the + // row is open + writeColumns(dmo, schema.attributeColumns(), false, omitInitial); + + // write all columns which must be serialized to elements as separate XML elements + writeColumns(dmo, schema.elementColumns(), false, omitInitial); + } + catch (IllegalAccessException | InvocationTargetException e) + { + return false; + } + + --indentLevel; + indent(); + writer.writeEndElement(); + lineBreak(); + + return true; + }); + } + + /** * Performs the export of a TEMP-TABLE schema. * * @param buffer @@ -526,28 +684,50 @@ { String tableName = schema.getTableName(); String legacyName = schema.getName(); + AbstractTempTable tempTable = (AbstractTempTable) buffer.buffer().tableHandle().get(); + String xmlNodeName = tempTable.getXmlNodeName().toJavaType(); + String nsPrefix = tempTable.namespacePrefix().toJavaType(); + String nsUri = tempTable.namespaceURI().toJavaType(); boolean isDataSet = buffer.isBeforeBuffer() || buffer.isAfterBuffer(); indent(); writer.writeStartElement("xsd:schema"); writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); - writer.writeAttribute("xmlns", ""); + writer.writeAttribute("xmlns", nsUri == null ? "" : nsUri); + if (nsPrefix != null && !nsPrefix.isEmpty() && nsUri != null) + { + writer.writeAttribute("xmlns:" + nsPrefix, nsUri); + writer.writeAttribute("targetNamespace", nsUri); + writer.writeAttribute("elementFormDefault", "qualified"); + } writer.writeAttribute("xmlns:prodata", "urn:schemas-progress-com:xml-prodata:0001"); lineBreak(); ++indentLevel; { indent(); writer.writeStartElement("xsd:element"); - writer.writeAttribute("name", tableName); - writer.writeAttribute("prodata:proTempTable", String.valueOf(isDataSet)); + writer.writeAttribute("name", xmlNodeName == null ? tableName : xmlNodeName); + writer.writeAttribute("prodata:proTempTable", "true"); // String.valueOf(isDataSet)); TODO: when is this attribute 'false' ? if (isDataSet && !legacyName.equals(tableName)) { writer.writeAttribute("prodata:tableName", legacyName); } - if (!minSchema) + + if (nsPrefix != null && !nsPrefix.isEmpty()) + { + writer.writeAttribute("prodata:prefix", nsPrefix); + } + + if (!minSchema && !schema.isNoUndo()) { writer.writeAttribute("prodata:undo", "true"); // TODO: where this value came from ? } + + if (xmlNodeName != null && !xmlNodeName.equals(tableName)) + { + writer.writeAttribute("prodata:tableName", tableName); + } + lineBreak(); ++indentLevel; { @@ -561,7 +741,8 @@ lineBreak(); ++indentLevel; { - if (!writeTableSchemaImpl(buffer, schema, tableName + "Row", defaultValues, minSchema, false)) + String rowName = (xmlNodeName == null ? tableName : xmlNodeName) + "Row"; + if (!writeTableSchemaImpl(buffer, schema, rowName, defaultValues, minSchema, false)) { return false; } @@ -601,7 +782,10 @@ { indent(); writer.writeEmptyElement("xsd:selector"); - writer.writeAttribute("xpath", ".//" + idxPair.getLeft()); + String idxName = (nsPrefix == null) + ? (".//" + idxPair.getLeft()) + : (".//" + nsPrefix + ":" + idxPair.getLeft()); + writer.writeAttribute("xpath", idxName); lineBreak(); String[] comps = index.getComponents(); @@ -610,7 +794,8 @@ { indent(); writer.writeEmptyElement("xsd:field"); - writer.writeAttribute("xpath", comps[i]); + String compName = (nsPrefix == null) ? comps[i] : (nsPrefix + ":" + comps[i]); + writer.writeAttribute("xpath", compName); if (!dirs[i]) { writer.writeAttribute("prodata:descending", "true"); @@ -631,8 +816,23 @@ writer.writeEndElement(); // xsd:element lineBreak(); + boolean hasIndexes = !minSchema && this.ds == null && !this.isSchemaOnly && !indexes.isEmpty(); + int idxCounter = 0; + if (hasIndexes) + { + idxCounter = indexes.size(); + for (Map.Entry> pair : indexes.entrySet()) + { + if (pair.getValue().getRight().isUnique()) + { + // decrement counter for unique indexes. The remainder is the count of non-unique indexes + --idxCounter; + } + } + } + // in case of standalone TEMP-TABLE schema serialization non unique indexes go here: - if (!minSchema && this.ds == null && !this.isSchemaOnly && !indexes.isEmpty()) + if (hasIndexes && idxCounter > 0) { indent(); writer.writeStartElement("xsd:annotation"); @@ -814,8 +1014,7 @@ } // dump all UNIQUE indexes here - for (Map.Entry> indexEntry : - indexes.entrySet()) + for (Map.Entry> indexEntry : indexes.entrySet()) { Pair idxPair = indexEntry.getValue(); TableMapper.LegacyIndexInfo index = idxPair.getRight(); @@ -998,13 +1197,14 @@ throws XMLStreamException { String legacyName = schema.getName(); + boolean isDataset = (ds != null); indent(); writer.writeStartElement("xsd:element"); writer.writeAttribute("name", tableName); writer.writeAttribute("minOccurs", "0"); writer.writeAttribute("maxOccurs", "unbounded"); - if (this.ds != null && !legacyName.equals(tableName)) + if (isDataset && !legacyName.equals(tableName)) { writer.writeAttribute("prodata:tableName", legacyName); } @@ -1013,7 +1213,8 @@ writer.writeAttribute("prodata:undo", "true"); // special flag for this? if (buffer.isAfterBuffer()) { - String beforeName = buffer.beforeBuffer().unwrap().name().toStringMessage(); + // using [buffer.beforeBuffer().unwrap().name()] will raise a [DeferredLegacyErrorException] + String beforeName = ((BufferImpl) buffer.beforeBuffer().getResource()).doGetName(); writer.writeAttribute("prodata:beforeTable", beforeName); } } @@ -1032,10 +1233,11 @@ { for (TempTableSchema.Column column : schema.columns()) { - String type = Util.xsdMap.get(column.getType()); + Class columnType = column.getType(); + String type = Util.xsdMap.get(columnType); if (type == null) { - ErrorManager.recordOrShowError(13078, Util.getLegacyType(column.getType())); + ErrorManager.recordOrShowError(13078, Util.getLegacyType(columnType)); // Unsupported data type for XML Serialization: . ErrorManager.recordOrShowError(13066, schema.getTableName()); // Unable to write XML Schema for temp-table . @@ -1044,22 +1246,43 @@ return false; } - String addType = Util.xsdProdataMap.get(column.getType()); + String addType = Util.xsdProdataMap.get(columnType); indent(); writer.writeEmptyElement("xsd:element"); writer.writeAttribute("name", column.getName()); writer.writeAttribute("type", type); writer.writeAttribute("nillable", String.valueOf(column.isNillable())); Integer extent = column.getExtent(); - if (extent != null) - { - writer.writeAttribute("minOccurs", String.valueOf(extent)); - writer.writeAttribute("maxOccurs", String.valueOf(extent)); + if (isDataset) + { + if (extent != null && extent > 0) + { + writer.writeAttribute("minOccurs", String.valueOf(extent)); + writer.writeAttribute("maxOccurs", String.valueOf(extent)); + } + } + else + { + if (!defaultValues) + { + writer.writeAttribute("minOccurs", "0"); + } + else if (extent != null && extent > 0) + { + writer.writeAttribute("minOccurs", String.valueOf(extent)); + } + + if (extent != null && extent > 0) + { + writer.writeAttribute("maxOccurs", String.valueOf(extent)); + } } if (!isSchemaOnly) { String fmt = column.getFormat(); - if (fmt != null && !fmt.isEmpty()) + + BaseDataType defValue = BaseDataType.generateDefault(columnType); + if (fmt != null && !fmt.isEmpty() && !fmt.equals(defValue.defaultFormatString())) { writer.writeAttribute("prodata:format", fmt); } @@ -1073,20 +1296,120 @@ { writer.writeAttribute("prodata:dataType", addType); } - if (defaultValues) + + if (isDataset) + { + if (defaultValues) + { + String initial = column.getInitial(); + String defVal = Util.defaultValuesMap.get(columnType); + if (initial != null && !initial.equals(defVal)) + { + writer.writeAttribute("prodata:initial", + initial.equals("?") ? "prodata:unknown" : initial); + } + } + } + else { String initial = column.getInitial(); - String defVal = Util.defaultValuesMap.get(column.getType()); - if (initial != null && !initial.equals(defVal)) - { - writer.writeAttribute("prodata:initial", - initial.equals("?") ? "prodata:unknown" : initial); - } - } - lineBreak(); - } - - DataSet ds = (DataSet) buffer.dataSet().getResource(); + if (initial != null) + { + boolean initialNull = "?".equals(initial); + if (!defaultValues || !initial.equals(Util.defaultValuesMap.get(columnType))) + { + if (!initialNull && type.contains("decimal") && !initial.contains(".")) + { + // use [decimal] property attribute instead? + initial += ".0"; + } + writer.writeAttribute("default", initialNull ? "prodata:unknown" : initial); + } + } + else if (!defaultValues) + { + initial = Util.defaultValuesMap.get(columnType); + if ("?".equals(initial)) + { + writer.writeAttribute("prodata:initial", "prodata:unknown"); + } + else + { + if (type.contains("decimal") && !initial.contains(".")) + { + // use [decimal] property attribute instead? + initial += ".0"; + } + writer.writeAttribute("default", initial); + } + } + } + + if (!minSchema && columnType == character.class && column.isCaseSensitive()) + { + writer.writeAttribute("prodata:caseSensitive", "true"); + } + if (!minSchema && columnType == decimal.class && column.getDecimals() > 0) + { + writer.writeAttribute("prodata:decimals", Integer.toString(column.getDecimals())); + } + lineBreak(); + } + + if (buffer.isBeforeBuffer() && ds == null) // only for TEMP-TABLE:WRITE-XML(SCHEMA) API + { + // + indent(); + writer.writeStartElement("xsd:element"); + writer.writeAttribute("name", Buffer.__ERROR_FLAG__); + writer.writeAttribute("type", "xsd:int"); + writer.writeAttribute("nillable", "true"); + writer.writeAttribute("prodata:initial", "prodata:unknown"); + writer.writeEndElement(); + lineBreak(); + + // + indent(); + writer.writeStartElement("xsd:element"); + writer.writeAttribute("name", Buffer.__ORIGIN_ROWID__); + writer.writeAttribute("type", "xsd:base64Binary"); + writer.writeAttribute("nillable", "true"); + writer.writeAttribute("prodata:dataType", "prodata:rowid"); + writer.writeEndElement(); + lineBreak(); + + // + indent(); + writer.writeStartElement("xsd:element"); + writer.writeAttribute("name", Buffer.__ERROR_STRING__); + writer.writeAttribute("type", "xsd:string"); + writer.writeAttribute("nillable", "true"); + writer.writeAttribute("prodata:initial", "prodata:unknown"); + writer.writeEndElement(); + lineBreak(); + + // + indent(); + writer.writeStartElement("xsd:element"); + writer.writeAttribute("name", Buffer.__AFTER_ROWID__); + writer.writeAttribute("type", "xsd:base64Binary"); + writer.writeAttribute("nillable", "true"); + writer.writeAttribute("prodata:dataType", "prodata:rowid"); + writer.writeEndElement(); + lineBreak(); + + // + indent(); + writer.writeStartElement("xsd:element"); + writer.writeAttribute("name", Buffer.__ROW_STATE__); + writer.writeAttribute("type", "xsd:int"); + writer.writeAttribute("nillable", "true"); + writer.writeAttribute("prodata:initial", "prodata:unknown"); + writer.writeEndElement(); + lineBreak(); + } + + DataSet ds = buffer._dataSet(); if (inDataset && ds != null) { List childRels = ds.getRelations(buffer, true, false, true); @@ -1134,7 +1457,7 @@ return true; } - /*** + /** * Writes the content of a TEMP-TABLE to serializer. * * @param relation @@ -1171,15 +1494,14 @@ { ErrorManager.recordOrShowError(13093); // Write temp-table data failed for WRITE-XML. - ErrorManager.recordOrShowError(13078, unsupportedType); + ErrorManager.recordOrThrowError(13078, unsupportedType, ""); // Unsupported data type for XML Serialization: . return false; } if (relation == null) { - // reads records in order of primary index, if available, else in ascending primary key - // order + // reads records in order of primary index, if available, else in ascending primary key order buffer.readAllRows( (dmo) -> writeRecord( buffer, dmo, rowName, schema, xmlns, includeHidden, includeProds, before, noInit)); @@ -1286,24 +1608,49 @@ // CREATED or MODIFIED before images writer.writeAttribute("prods:id", rowName + dmo._peerRowid()); } - if (rowState != Buffer.ROW_UNMODIFIED) + + if (!before) { - writer.writeAttribute("prods:rowState", Util.getRowStateAsString(rowState)); + if (rowState != Buffer.ROW_UNMODIFIED) + { + writer.writeAttribute("prods:rowState", Util.getRowStateAsString(rowState)); + } + + Integer errorFlags = dmo._errorFlags(); + if (errorFlags != null && errorFlags != 0) // also only for 'changes' dataset (?) + { + writer.writeAttribute("prods:hasErrors", "true"); + } + + String errorString = dmo._errorString(); + if (errorString != null) // also only for 'changes' dataset (?) + { + prodsErrors.put(rowName + dmo.primaryKey(), Pair.of(rowName, errorString)); + } } } ++indentLevel; - // write all columns which must be serialized to attributes first, while the start - // element for the row is open - writeColumns(dmo, schema.attributeColumns(), hidden); + // write all columns which must be serialized to attributes first, while the start element for the + // row is open + writeColumns(dmo, schema.attributeColumns(), hidden, omitInitialValues); lineBreak(); // write all columns which must be serialized to elements as separate XML elements - writeColumns(dmo, schema.elementColumns(), hidden); + writeColumns(dmo, schema.elementColumns(), hidden, omitInitialValues); BufferImpl buf = (BufferImpl) buffer.getDMOProxy(); - DataSet ds = (DataSet) buf.dataSet().getResource(); + if (buf.isBeforeBuffer() && ds == null) + { + writeDatum(ERROR_FLAG, new integer(dmo._errorFlags())); + writeDatum(ORIGIN_ROWID, new rowid(dmo._originRowid())); + writeDatum(ERROR_STRING, new character(dmo._errorString())); + writeDatum(AFTER_ROWID, new rowid(dmo._peerRowid())); + writeDatum(ROW_STATE, new integer(dmo._rowState())); + } + + DataSet ds = buf._dataSet(); if (this.ds != null && ds != null) { // position the buffer on this DMO @@ -1359,7 +1706,8 @@ */ private void writeColumns(Record dmo, Iterable columns, - boolean hidden) + boolean hidden, + boolean noInit) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, @@ -1376,8 +1724,26 @@ Integer extent = column.getExtent(); Method getter = column.getGetter(); - if (extent != null) + if (extent != null && extent > 0) { + if (noInit) + { + boolean untouched = true; + for (int i = 0; i < extent; i++) + { + if (column.isChanged((BaseDataType) column.getGetter().invoke(dmo, i))) + { + untouched = false; + break; // drop all other tests from this extent + } + } + + if (untouched) + { + continue; // skip to next column + } + } + for (int i = 0; i < extent; i++) { BaseDataType datum = (BaseDataType) getter.invoke(dmo, i); @@ -1386,6 +1752,14 @@ } else { + if (noInit) + { + if (!column.isChanged((BaseDataType) column.getGetter().invoke(dmo))) + { + continue; // skip to next column + } + } + BaseDataType datum = (BaseDataType) getter.invoke(dmo); writeDatum(column, datum); } @@ -1407,8 +1781,8 @@ private void writeDatum(TempTableSchema.Column column, BaseDataType datum) throws XMLStreamException { - String name = column.getName(); - String value = null; + String name = ttNsPrefix != null ? (ttNsPrefix + ":" + column.getName()) : column.getName(); + String value; if (datum instanceof date) { // this includes datetime and datetime-tz, too @@ -1424,22 +1798,34 @@ } else if (datum instanceof raw || datum instanceof blob) { - Base64.Encoder enc = Base64.getEncoder(); - value = enc.encodeToString(((BinaryData) datum).getByteArray()); + if (!datum.isUnknown()) + { + Base64.Encoder enc = Base64.getEncoder(); + value = enc.encodeToString(((BinaryData) datum).getByteArray()); + } + else + { + value = ""; // or null + } } else { value = datum.toStringMessage(); - if (datum instanceof decimal && !value.contains(".")) - { - value += ".0"; - } - else if (datum instanceof raw && ((raw)datum).length().longValue() == 0L) - { - value = ""; + if (datum instanceof decimal) + { + int colDecs = column.getDecimals(); + if (colDecs > 0) + { + value = ((decimal) datum).toJavaType().setScale(colDecs, BigDecimal.ROUND_HALF_UP).toString(); + } + else if (!value.contains(".")) + { + // use [decimal] property attribute instead? + value += ".0"; + } } } - + // no need to escape, the writer will automatically escape the value switch (column.getNodeType()) @@ -1533,7 +1919,7 @@ return (char) c; } }); - try (OutputStream stream = target.getOutputStream()) + try (OutputStream stream = target.getStream()) { try { === modified file 'src/com/goldencode/p2j/persist/serial/XmlImport.java' --- src/com/goldencode/p2j/persist/serial/XmlImport.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/persist/serial/XmlImport.java 2021-01-27 01:54:24 +0000 @@ -2,9 +2,9 @@ ** Module : XmlImport.java ** Abstract : Read XML content into a temp-table. ** -** Copyright (c) 2017-2020, Golden Code Development Corporation. +** Copyright (c) 2017-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20171016 Created initial version. ** 002 CA 20180511 Added XML-NODE-NAME table option support. ** 003 ECF 20190123 Replaced TempTableSchema with XmlTempTableSchema. Refactored ExtentTracker @@ -14,6 +14,11 @@ ** 006 OM 20190909 Added TempTable read and schema validation. ** 007 ECF 20200906 New ORM implementation. ** CA 20200906 Batch error fix. +** 008 OM 20201120 Added missing features: useLobs, schema location, field mapping. +** Fixed restoring table/fields attributes: case-sensitivity, decimals, xml uri, xml prefix, +** xml namespace, read-mode, etc +** 20210106 Dropped false parameter of TEMP-TABLE-PREPARE API. +** 20210108 Improve error handling when reading XML serialized files. */ /* @@ -72,12 +77,17 @@ package com.goldencode.p2j.persist.serial; import java.io.*; +import java.lang.reflect.*; import java.util.*; +import java.util.logging.*; import javax.xml.namespace.*; import javax.xml.stream.*; +import com.goldencode.p2j.convert.*; import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.lock.*; +import com.goldencode.p2j.persist.orm.*; import com.goldencode.p2j.util.*; +import com.goldencode.p2j.util.ErrorManager; import com.goldencode.util.CaseInsensitiveString; import static com.goldencode.p2j.persist.serial.SerializeOptions.*; @@ -92,6 +102,9 @@ */ public final class XmlImport { + /** Logger for objects of this type. */ + private static final Logger log = LogHelper.getLogger(XmlImport.class.getName()); + /** XSD schema element name */ private static final String ELEM_SCHEMA = "schema"; @@ -107,6 +120,9 @@ /** Verify schema mode. */ private final Verify verifySchemaMode; + /** Use CLOB/BLOB instead of character/raw type? (Value of {@code override-default-mapping} parameter). */ + private final boolean useLobs; + /** Buffer proxy. */ private Buffer proxy; @@ -122,12 +138,24 @@ /** Current dataset, if one is being processed. */ private DataSet ds = null; + /** Current temp-table, if one is being processed. */ + private AbstractTempTable tt = null; + + /** The future name of the read temp-table. */ + private String ttName = null; + + /** The desired mapping of fields to new type, if one is defined. */ + private Map fieldTypeMapping = null; + + /** 'External' schema location, if one is provided. */ + private String schemaLocation = null; + /** * The set of temp-table builders for a dataset. They are mapped by their (XML-) names. They * are collected while parsing the schema and since their indexes are available only at the end * they are TABLE-PREPARED only on DataSet's end element. */ - private Map tables = new LinkedHashMap<>(); + private final Map tables = new LinkedHashMap<>(); /** The mapping of the legacy table names by their (case-insensitive) XML name. */ private Map tablesByXmlName = null; @@ -185,13 +213,16 @@ String readMode, String schemaLocation, boolean overrideDefaultMapping, - String fieldTypeMapping, + Map fieldTypeMapping, String verifySchemaMode) { this.source = source; this.readMode = (readMode == null) ? Read.MERGE : Read.valueOf(readMode.toUpperCase()); + this.useLobs = overrideDefaultMapping; this.verifySchemaMode = (verifySchemaMode == null) ? Verify.LOOSE : Verify.valueOf(verifySchemaMode.toUpperCase()); + this.fieldTypeMapping = fieldTypeMapping; + this.schemaLocation = schemaLocation; } /** @@ -206,15 +237,20 @@ */ public boolean importDatasetSchema(DataSet ds) { + if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA, LegacyResource.DATASET + " widget")) + { + return false; + } + this.ds = ds; this.tablesByXmlName = new HashMap<>(); this.relations = new LinkedList<>(); - try (InputStream stream = source.getInputStream()) + try (InputStream stream = source.getStream()) { reader = XMLInputFactory.newInstance().createXMLStreamReader(stream); - if (!readDatasetSchema()) + if (!readDatasetSchema(true)) { ErrorManager.recordOrShowError(13033); // Verify Temp-Table or dataset schema against XML Schema failed. @@ -247,6 +283,71 @@ } /** + * Configures a temp-table schema or validates it against the XML file. The other parameters are set in + * the constructor. + * + * @param tt + * The destination for read schema. + * + * @return {@code true} on success. On failure, {@code false} is returned and the eventual error is + * already displayed. + */ + public boolean importTempTableSchema(AbstractTempTable tt) + { + if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA, + LegacyResource.TEMP_TABLE + " widget")) + { + return false; + } + + this.tt = tt; + this.tablesByXmlName = new HashMap<>(); + + try (InputStream stream = source.getStream()) + { + reader = XMLInputFactory.newInstance().createXMLStreamReader(stream); + + if (!readTableSchema((String) null, null, null)) + { + if (tt.prepared().toJavaType()) + { + ErrorManager.recordOrShowError(13033); + // Verify Temp-Table or dataset schema against XML Schema failed. + } + else + { + ErrorManager.recordOrShowError(13032); + // Unable to create Temp-Table or dataset schema from XML Schema. + } + return false; + } + return true; + } + catch (IOException | XMLStreamException exc) + { + return false; + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (XMLStreamException exc) + { + // TODO: log + } + reader = null; + } + + this.tt = null; + this.tablesByXmlName = null; + } + } + + /** * Imports a dataset. The other parameters are set in constructor. * * @param dataSet @@ -257,6 +358,11 @@ */ public boolean importDataset(DataSet dataSet) { + if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.DATASET + " widget")) + { + return false; + } + this.ds = dataSet; this.tablesByXmlName = new HashMap<>(); this.relations = new LinkedList<>(); @@ -268,7 +374,7 @@ } XMLInputFactory factory = XMLInputFactory.newInstance(); - try (InputStream stream = source.getInputStream()) + try (InputStream stream = source.getStream()) { reader = factory.createXMLStreamReader(stream); boolean isRoot = true; @@ -286,7 +392,7 @@ case ELEM_SCHEMA: // the [schema] node is lost at this moment and "xmlns:xsd", "xmlns", and // "xmlns:prodata" attributes might not be accessible - if (!readDatasetSchema()) + if (!readDatasetSchema(false)) { ErrorManager.recordOrShowError(13033); // Verify Temp-Table or dataset schema against XML Schema failed. @@ -324,12 +430,29 @@ } if (!found) { - // TODO: report error - return false; - } - - this.schema = new XmlTempTableSchema((TemporaryBuffer) buffer.buffer()); - this.proxy = (Buffer) buffer.buffer().getDMOProxy(); + // if the table is not found in database, the record is simply skipped + if (!skipRecord(name)) + { + return false; + } + break; + } + + TemporaryBuffer ttBuffer = (TemporaryBuffer) buffer.buffer(); + if (readMode == Read.REPLACE && !readingBefore) + { + // the REPLACE mode needs primary unique index declared for the after table + P2JIndex primaryIndex = ttBuffer.getDmoInfo().getPrimaryIndex(true); + if (primaryIndex == null) + { + ErrorManager.recordOrShowError(13063, ttBuffer.getDmoInfo().legacyTable); + // REPLACE mode requires a unique primary index in the target table
. + return false; + } + } + + this.schema = new XmlTempTableSchema(ttBuffer); + this.proxy = (Buffer) ttBuffer.getDMOProxy(); this.extentTracker = new ExtentTracker(this.schema); if (!readRecord()) @@ -348,14 +471,16 @@ } catch (IOException | XMLStreamException | PersistenceException exc) { - exc.printStackTrace(); return false; } catch (Throwable exc) { - exc.printStackTrace(); - System.out.println(reader.getLocation()); - + // intercept all exceptions + if (log.isLoggable(Level.SEVERE)) + { + log.log(Level.SEVERE, exc.getMessage() + reader.getLocation(), exc); + } + // the rethrow it back throw exc; } finally @@ -392,7 +517,15 @@ */ public boolean importTable(AbstractTempTable tempTable) { - knownTableSchema = !tempTable._dynamic() || ((TempTableBuilder) tempTable)._prepared(); + if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.TEMP_TABLE + " widget")) + { + return false; + } + + String nsPrefix = null; + String nsURI = null; + + knownTableSchema = !tempTable._dynamic() || tempTable._prepared(); if (knownTableSchema) { @@ -406,9 +539,19 @@ proxy.deleteAll(); } } + else if (schemaLocation != null) + { + XmlImport xmlImport = new XmlImport(this.source, null, null, this.useLobs, + this.fieldTypeMapping, this.verifySchemaMode.toString()); + boolean b = xmlImport.importTempTableSchema(tempTable); + if (!b) + { + return false; + } + } XMLInputFactory factory = XMLInputFactory.newInstance(); - try (InputStream stream = source.getInputStream()) + try (InputStream stream = source.getStream()) { reader = factory.createXMLStreamReader(stream); String tableName = schema == null ? null : schema.getTableName(); @@ -422,6 +565,8 @@ case XMLStreamReader.START_ELEMENT: if (isRoot) { + nsPrefix = reader.getPrefix(); + nsURI = reader.getNamespaceURI(); isRoot = false; break; } @@ -430,7 +575,7 @@ if (ELEM_SCHEMA.equals(name)) { - if (!readTableSchema(tempTable)) + if (!readTableSchema(tempTable, nsPrefix, nsURI)) { ErrorManager.recordOrShowError(13033); // Verify Temp-Table or dataset schema against XML Schema failed. @@ -451,7 +596,7 @@ return false; } - String rowName = tableName + "Row"; + String rowName = tempTable.getXmlNodeName().toJavaType() + "Row"; if (rowName.equalsIgnoreCase(name)) { if (!readRecord()) @@ -473,8 +618,33 @@ } } } - catch (IOException | XMLStreamException | PersistenceException exc) - { + catch (XMLStreamException xexc) + { + ErrorManager.recordOrShowError(13035, source.getFileName(), ""); + // Error reading XML file ''. + + int line = xexc.getLocation().getLineNumber(); + int col = xexc.getLocation().getColumnNumber(); + String err = "FATAL ERROR: file '" + source.getFileName() + "', line '" + line + + "', column '" + col + "', message '" + xexc.getMessage().replace('\n', ' ') + "'"; + ErrorManager.recordOrShowError(13064, err, ""); + // READ-XML encountered an error while parsing the XML Document: . (13064) + return false; + } + catch (PersistenceException pexc) + { + // intercept all exceptions + if (log.isLoggable(Level.SEVERE)) + { + log.log(Level.SEVERE, pexc.getMessage() + reader.getLocation(), pexc); + } + return false; + } + catch (IOException exc) + { + // this is a bit strange: + ErrorManager.recordOrShowError(4065, "READ-XML", "TEMP-TABLE widget"); + // **The attribute on the has invalid arguments. return false; } finally @@ -498,18 +668,24 @@ /** * Reads and processes a {@code TEMP-TABLE} schema. * + * @param nsPrefix + * The detected XML namespace prefix. + * @param nsUri + * The detected XML namespace URI. + * * @return {@code true} on success and {@code false} otherwise. When {@code false} is returned - * then the error message has already been dispatched to {@code ErrorManager}. + * then the error message has already been dispatched to {@code ErrorManager}. * * @throws XMLStreamException * If a XML-related error occurs (like an unexpected end of stream). */ - private boolean readTableSchema(AbstractTempTable tempTable) + private boolean readTableSchema(AbstractTempTable tempTable, String nsPrefix, String nsUri) throws XMLStreamException { TempTableBuilder ttb = tempTable._dynamic() ? (TempTableBuilder) tempTable : null; String legacyTableName = null; String xmlName = null; + String undoable = null; List readFields = new LinkedList<>(); while (reader.hasNext()) @@ -534,6 +710,7 @@ // this is the table definition legacyTableName = getXmlAttribute("prodata:tableName"); xmlName = getXmlAttribute("name"); + undoable = getXmlAttribute("prodata:undo"); if (ttb != null) { @@ -543,30 +720,62 @@ break; } - String fieldName = getXmlAttribute("name"); String fieldType = getXmlAttribute("type"); - String extentMax = getXmlAttribute("maxOccurs"); - String ablDataType = getXmlAttribute("prodata:dataType"); - String nullable = getXmlAttribute("nullable"); if (fieldType != null) { + String fieldName = getXmlAttribute("name"); + String extentMax = getXmlAttribute("maxOccurs"); + String ablDataType = getXmlAttribute("prodata:dataType"); + String nullable = getXmlAttribute("nullable"); + String defaultStrVal = getXmlAttribute("default"); + String decimals = getXmlAttribute("prodata:decimals"); + String format = getXmlAttribute("prodata:format"); + String cs = getXmlAttribute("prodata:caseSensitive"); + String optional = getXmlAttribute("nillable"); + // this a field definition - String ablType = get4GLType(fieldType, ablDataType, nullable); + String ablType = get4GLType( + fieldType, ablDataType, nullable, useLobs, legacyTableName, fieldName); if (ttb != null) { - // constructing table from schema - if (extentMax == null) - { - // plain, no extent - ttb.addNewField(new character(fieldName), new character(ablType)); - } - else - { - ttb.addNewField(new character(fieldName), - new character(ablType), - new integer(Integer.parseInt(extentMax))); - } + ParmType parmType = TempTableBuilder.validateDatatype(ablType); + if (parmType == null) + { + ErrorManager.recordOrShowError(13201, ablType, fieldName); + // Invalid data type override '' for field ''. + ErrorManager.recordOrShowError(13032); + // Unable to create Temp-Table or dataset schema from XML Schema. + return false; + } + + if (format == null) + { + format = TempTableBuilder.getDefaultDataTypeFormat(ablType); + } + long extent = extentMax == null ? 0 : Long.parseLong(extentMax); + BaseDataType defVal = null; + if (BaseDataType.class.isAssignableFrom(parmType.getParamType()) && + defaultStrVal != null) + { + try + { + Constructor ctor = + (Constructor) + parmType.getParamType().getConstructor(String.class); + defVal = ctor.newInstance(defaultStrVal); + } + catch (NoSuchMethodException | IllegalAccessException | + InstantiationException | InvocationTargetException e) + { + return false; + } + } + + ttb.addField(new P2JField(fieldName, parmType, extent, format, defVal, null, null, + Boolean.parseBoolean(cs), null, null, false, null, + null, null, null, !Boolean.parseBoolean(optional), + decimals == null ? 0 : Integer.parseInt(decimals))); } else if (verifySchemaMode != Verify.IGNORE) { @@ -600,7 +809,7 @@ case "unique": // found an unique index String primary = getXmlAttribute("prodata:primaryIndex"); - if (!readIndex(getXmlAttribute("name"), Boolean.parseBoolean(primary), true)) + if (!readIndex(getXmlAttribute("name"), nsPrefix, Boolean.parseBoolean(primary), true)) { return false; } @@ -608,7 +817,7 @@ case "index": // found an non-unique index - if (!readIndex(getXmlAttribute("name"), false, false)) + if (!readIndex(getXmlAttribute("name"), nsPrefix, false, false)) { return false; } @@ -645,23 +854,26 @@ } } } - this.schema.setXmlTableName(xmlName); return true; } - logical ok = ttb.tempTablePrepare(legacyTableName, true); // TODO: where do we get [before] flag? - if (!ok.getValue()) + if (!ttb._prepared()) { - // TODO: report error? already reported? - return false; + ttb.setCanUndo(new logical(Boolean.parseBoolean(undoable))); + logical ok = ttb.tempTablePrepare(legacyTableName); + if (!ok.getValue()) + { + // TODO: report error? already reported? + return false; + } + ttb.setXmlNodeName(xmlName); + ttb.namespaceURI(nsUri); + ttb.namespacePrefix(nsPrefix); + BufferImpl defaultBuffer = (BufferImpl) ttb.defaultBufferHandle().getResource(); + this.schema = new XmlTempTableSchema((TemporaryBuffer) defaultBuffer.buffer()); + this.extentTracker = new ExtentTracker(schema); + this.proxy = (Buffer) defaultBuffer.buffer().getDMOProxy(); } - // TODO: [AbstractTempTable] must implement [XmlNode] - // ttb.setXmlNodeName(xmlName); - BufferImpl defaultBuffer = (BufferImpl) ttb.defaultBufferHandle().getResource(); - this.schema = new XmlTempTableSchema((TemporaryBuffer) defaultBuffer.buffer()); - this.schema.setXmlTableName(xmlName); - this.extentTracker = new ExtentTracker(schema); - this.proxy = (Buffer) defaultBuffer.buffer().getDMOProxy(); return true; } @@ -676,6 +888,10 @@ * Reads and processes a {@code DataSet} schema. If the {@code DataSet} is in CLEAR state, the * structure is initialized and its tables and relations are set up. Otherwise the schema is * checked according to {@code verifySchemaMode}. + * + * @param verify + * When {@code true} the schema is only verified. Otherwise the methods attempts to create the + * read schema into the dataset. * * @return {@code true} on success and {@code false} otherwise. When {@code false} is returned * then the error message has already been dispatched to {@code ErrorManager}. @@ -683,7 +899,7 @@ * @throws XMLStreamException * If a XML-related error occurs (like an unexpected end of stream). */ - private boolean readDatasetSchema() + private boolean readDatasetSchema(boolean verify) throws XMLStreamException { while (reader.hasNext()) @@ -723,10 +939,21 @@ String tableName = getXmlAttribute("prodata:tableName"); if (tableName == null) { - // if dedicated attribute not found, use the xml name attribute + // if dedicated attribute not found, use the xml name attribute tableName = getXmlAttribute("name"); } - if (!readTableSchema(tableName)) + handle buffer = ds.bufferHandle(tableName); + if (verifySchemaMode == Verify.STRICT && verify && !buffer._isValid()) + { + // TODO: 4GL will test the other way around: collects all table definitions from + // file and attempt to match dataset in that collection, throwing 10779 + // errors first. Then the difference is computed and 13138 is eventually raised + + ErrorManager.recordOrShowError(13138, tableName); + // Temp-table '
' not found in XML Schema + return false; + } + if (!readTableSchema(tableName, null)) { return false; } @@ -740,7 +967,7 @@ case "unique": // found an unique index String primary = getXmlAttribute("prodata:primaryIndex"); - if (!readIndex(getXmlAttribute("name"), Boolean.parseBoolean(primary), true)) + if (!readIndex(getXmlAttribute("name"), null, Boolean.parseBoolean(primary), true)) { return false; } @@ -748,7 +975,7 @@ case "index": // found an non-unique index - if (!readIndex(getXmlAttribute("name"), false, false)) + if (!readIndex(getXmlAttribute("name"), null, false, false)) { return false; } @@ -768,7 +995,7 @@ case "sequence": case "annotation": case "appinfo": - // tolerate them, nothing to do ta this time + // tolerate them, nothing to do at this time break; } break; @@ -793,7 +1020,7 @@ { xmlName = tablesByXmlName.get(new CaseInsensitiveString(xmlName)); } - TempTableBuilder ttb = tables.get(xmlName); + AbstractTempTable ttb = tables.get(xmlName); if (ttb == null) { ErrorManager.recordOrShowError(13079, xmlName); @@ -803,7 +1030,7 @@ } // check the other way around: all read tables must be present in DS: - for (Map.Entry entry : tables.entrySet()) + for (Map.Entry entry : tables.entrySet()) { String dstt = entry.getKey(); if (!ds.bufferHandle(dstt)._isValid()) @@ -870,25 +1097,37 @@ String xmlName = schema.getTableName(); if (xmlName != null && !xmlName.equals(buff.doGetName())) { - tablesByXmlName.put(new CaseInsensitiveString(xmlName), - buff.doGetName()); + tablesByXmlName.put(new CaseInsensitiveString(xmlName), buff.doGetName()); } } return true; } - // create tables from [tables] - Buffer[] dsBuffs = new Buffer[tables.size()]; - int k = 0; - for (Map.Entry entry : tables.entrySet()) + for (Map.Entry entry : tables.entrySet()) { - TempTableBuilder ttb = entry.getValue(); - - logical ok = ttb.tempTablePrepare(entry.getKey(), true); // TODO: where do we get [before] flag? + AbstractTempTable atb = entry.getValue(); + if (atb instanceof StaticTempTable) + { + continue; + } + + TempTableBuilder ttb = (TempTableBuilder) atb; + if (ttb._prepared()) + { + continue; + } + + logical ok = ttb.tempTablePrepare(entry.getKey()); if (ok.getValue()) { - dsBuffs[k++] = (BufferImpl) ttb.defaultBufferHandle().getResource(); + // set them in dataset: + BufferImpl defBuffer = (BufferImpl) ttb.defaultBufferHandle().getResource(); + if (!ds.addBuffer(defBuffer).getValue()) + { + // TODO: report error? already reported? + return false; + } } else { @@ -897,23 +1136,24 @@ } } - // set them in dataset: - if (!ds.setBuffers(dsBuffs).getValue()) - { - return false; - } - - // add the relations, if any: - for (Relation rel : relations) - { - handle hRel = ds.addRelation(ds.bufferHandle(rel.parent), - ds.bufferHandle(rel.child), - new character(rel.fields)); - DataRelation relation = (DataRelation) hRel.getResource(); - if (relation != null) + if (ds._dynamic() && verifySchemaMode != Verify.STRICT) // if (!verify) + { + // add the relations, if not already added (?) + for (Relation rel : relations) { - relation.name(rel.name); - relation.setNested(rel.nested); + handle hRel = ds.addRelationImpl( + ds.bufferHandle(rel.parent), ds.bufferHandle(rel.child), + new character(rel.fields), null, null, null, null, null, true); + if (hRel == null) + { + continue; // duplicate relation + } + DataRelation relation = (DataRelation) hRel.getResource(); + if (relation != null) + { + relation.name(rel.name); + relation.setNested(rel.nested); + } } } return true; @@ -926,10 +1166,151 @@ } /** + * Reads and processes an {@code AbstractTempTable} schema. If the {@code TempTable} is in CLEAR state, the + * structure is initialized. Otherwise the schema is checked according to {@code verifySchemaMode}. + * + * @param xmlNodeName + * The detected XML node name. + * @param nsPrefix + * The detected XML namespace prefix. + * @param nsUri + * The detected XML namespace URI. + * + * @return {@code true} on success and {@code false} otherwise. When {@code false} is returned + * then the error message has already been dispatched to {@code ErrorManager}. + * + * @throws XMLStreamException + * If a XML-related error occurs (like an unexpected end of stream). + */ + private boolean readTableSchema(String xmlNodeName, String nsPrefix, String nsUri) + throws XMLStreamException + { + boolean undo = false; + while (reader.hasNext()) + { + int type = reader.next(); + String name = null; + switch (type) + { + case XMLStreamReader.START_ELEMENT: + if (verifySchemaMode == Verify.IGNORE && !ds._dynamic()) + { + // for static datasets, IGNORE mode will ignore all schema nodes + break; + } + + name = reader.getLocalName(); + switch (name) + { + case "element": + boolean isTT = Boolean.parseBoolean(getXmlAttribute("prodata:proTempTable")); + if (isTT) + { + String tableName = getXmlAttribute("prodata:tableName"); + if (tableName == null) + { + // if dedicated attribute not found, use the xml name attribute + tableName = getXmlAttribute("name"); + } + else if (xmlNodeName == null && !tableName.equals(getXmlAttribute("name"))) + { + xmlNodeName = getXmlAttribute("name"); + } + if (nsPrefix == null) + { + nsPrefix = getXmlAttribute("prodata:prefix"); + } + undo = Boolean.parseBoolean(getXmlAttribute("prodata:undo")); + + ttName = tableName; + if (!readTableSchema(tableName, nsPrefix)) + { + return false; + } + } + else + { + // what? log something? + return false; + } + break; + + case ELEM_SCHEMA: + if (nsUri == null) + { + nsUri = getXmlAttribute("targetNamespace"); + } + // check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes + break; + + case "unique": + // found an unique index + String primary = getXmlAttribute("prodata:primaryIndex"); + if (!readIndex(getXmlAttribute("name"), nsPrefix, Boolean.parseBoolean(primary), true)) + { + return false; + } + break; + + case "index": + // found an non-unique index + if (!readIndex(getXmlAttribute("name"), nsPrefix, false, false)) + { + return false; + } + break; + + case "complexType": + case "sequence": + case "annotation": + case "appinfo": + // tolerate them, nothing to do at this time + break; + } + break; + + case XMLStreamReader.END_ELEMENT: + name = reader.getLocalName(); + if (ELEM_SCHEMA.equals(name)) + { + if (tt._dynamic() && !tt.prepared().booleanValue()) + { + TempTableBuilder ttb = (TempTableBuilder) tt; + ttb.setCanUndo(new logical(undo)); + if (xmlNodeName != null) + { + ttb.setXmlNodeName(xmlNodeName); + } + if (nsPrefix != null) + { + ttb.namespacePrefix(nsPrefix); + } + if (nsUri != null) + { + ttb.namespaceURI(nsUri); + } + if (!ttb.tempTablePrepare(ttName).booleanValue()) + { + // TODO: report error? already reported? + return false; + } + } + } + break; + } + } + + return true; + //throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation()); + } + + /** * Reads and process an index from the XML stream. * * @param indexName * The index name. + * @param nsPrefix + * The detected XML namespace prefix. * @param primary * {@code true} if this is a primary index. * @param unique @@ -941,10 +1322,10 @@ * @throws XMLStreamException * If a XML-related error occurs (like an unexpected end of stream). */ - private boolean readIndex(String indexName, boolean primary, boolean unique) + private boolean readIndex(String indexName, String nsPrefix, boolean primary, boolean unique) throws XMLStreamException { - TempTableBuilder ttb = null; + AbstractTempTable ttb = null; while (reader.hasNext()) { int type = reader.next(); @@ -956,33 +1337,65 @@ { case "table": // in case of NON-unique indexes ttb = tables.get(getXmlAttribute("name")); - ttb.addNewIndex(new character(indexName), - new logical(unique), - new logical(primary)); + if (ttb != null && !ttb._prepared()) + { + ttb.addNewIndex(new character(indexName), new logical(unique), new logical(primary)); + } + // if the table is already prepared, the indexes in schema are not verified, not even if + // STRICT mode was specified. break; case "selector": // in case of unique indexes String xpath = getXmlAttribute("xpath"); if (xpath.startsWith(".//")) { - xpath = tablesByXmlName.get(new CaseInsensitiveString(xpath.substring(3))); - } + xpath = xpath.substring(3); + } + + if (nsPrefix != null && xpath.startsWith(nsPrefix)) + { + // drop the [nsPrefix], including the COLON, if any + xpath = xpath.substring(nsPrefix.length() + 1); + } + + if (tablesByXmlName != null) + { + xpath = tablesByXmlName.get(new CaseInsensitiveString(xpath)); + } + else if (xpath.endsWith("Row")) + { + xpath = xpath.substring(0, xpath.length() - 3); + } + ttb = tables.get(xpath); - ttb.addNewIndex(new character(indexName), - new logical(unique), - new logical(primary)); + if (ttb != null && !ttb._prepared()) + { + ttb.addNewIndex(new character(indexName), new logical(unique), new logical(primary)); + } + // if the table is already prepared, the indexes in schema are not verified, not even if + // STRICT mode was specified. break; case "field": String field = getXmlAttribute(unique ? "xpath" : "name"); + if (nsPrefix != null && field.startsWith(nsPrefix)) + { + // drop the [nsPrefix], including the COLON, if any + field = field.substring(nsPrefix.length() + 1); + } boolean desc = Boolean.parseBoolean(getXmlAttribute("prodata:descending")); - ttb.addFieldToIndex(new character(indexName), - new character(field), - new character(desc ? "desc" : "asc")); + if (ttb != null && !ttb._prepared()) + { + ttb.addFieldToIndex(new character(indexName), + new character(field), + new character(desc ? "desc" : "asc")); + } + // if the table is already prepared, the indexes in schema are not verified, not even if + // STRICT mode was specified. break; } break; - + case XMLStreamReader.END_ELEMENT: String localName = reader.getLocalName(); if ("unique".equals(localName) || "index".equals(localName)) @@ -1090,19 +1503,46 @@ * * @param tableName * The name of the table which is being processed. - * - * @return {@code true} if the operation is successful. If the operation failed {@code false} - * is returned. - * - * @throws XMLStreamException + * @param nsPrefix + * The detected XML namespace prefix. + * + * @return {@code true} if the operation is successful. If the operation fails, {@code false} is returned. + * + * @throws XMLStreamException + * in the event of an exception occurred while reading the XML stream. */ - private boolean readTableSchema(String tableName) + private boolean readTableSchema(String tableName, String nsPrefix) throws XMLStreamException { - TempTableBuilder ttb = new TempTableBuilder(); - this.tables.put(tableName, ttb); - this.tablesByXmlName.put(new CaseInsensitiveString(getXmlAttribute("name")), tableName); - + // detect the working mode + AbstractTempTable att = null; + DmoMeta toMatch = null; // the DMO to verify against + if (ds != null) + { + BufferImpl existingBuffer = (BufferImpl) ds.bufferHandle(tableName).getResource(); + if (existingBuffer != null) + { + // if the default buffer of the table already exists we need just to verify the schema of the + // static/dynamic table, not to create it from the ground up. In this case [att] is null. + toMatch = existingBuffer.buffer().getDmoInfo(); + att = (AbstractTempTable) existingBuffer.buffer().tableHandle().getResource(); + } + } + if (toMatch == null) + { + att = (tt == null) ? new TempTableBuilder() : (TempTableBuilder) tt; + } + this.tables.put(tableName, att); + String xmlName = getXmlAttribute("name"); + this.tablesByXmlName.put(new CaseInsensitiveString(xmlName), tableName); + + Set fieldsToMatch = null; + if (verifySchemaMode == Verify.STRICT && toMatch != null) + { + fieldsToMatch = new LinkedHashSet<>(); + toMatch.getFields(false).forEachRemaining(fieldsToMatch::add); + } + // allows to distinct between table-ELEMENT and field-ELEMENT boolean moreFields = true; while (reader.hasNext()) @@ -1125,7 +1565,7 @@ { // in this case the read [name] attribute is, in fact, the name of the nested table String legacy = getXmlAttribute("prodata:tableName"); - if (!readTableSchema(legacy == null ? fieldName : legacy)) + if (!readTableSchema(legacy == null ? fieldName : legacy, nsPrefix)) { return false; } @@ -1134,18 +1574,114 @@ { String ablType = get4GLType(type, getXmlAttribute("prodata:dataType"), - getXmlAttribute("nullable")); - if (extentMax == null) - { - // plain, no extent - ttb.addNewField(new character(fieldName), new character(ablType)); - } - else - { - ttb.addNewField(new character(fieldName), - new character(ablType), - new integer(Integer.parseInt(extentMax))); - } + getXmlAttribute("nullable"), + useLobs && toMatch == null, + tableName, + fieldName); + boolean cs = Boolean.parseBoolean(getXmlAttribute("prodata:caseSensitive")); + int decimals; + try + { + decimals = Integer.parseInt(getXmlAttribute("prodata:decimals"), 10); + } + catch (NumberFormatException e) + { + decimals = 0; + } + + String defaultStrVal = getXmlAttribute("default"); + + ParmType fieldType = TempTableBuilder.validateDatatype(ablType); + if (fieldType == null) + { + ErrorManager.recordOrShowError(13201, ablType, fieldName); + // Invalid data type override '' for field ''. + ErrorManager.recordOrShowError(13032); + // Unable to create Temp-Table or dataset schema from XML Schema. + return false; + } + + String format = TempTableBuilder.getDefaultDataTypeFormat(ablType); + long extent = extentMax == null ? 0 : Long.parseLong(extentMax); + + BaseDataType defVal = null; + Class fwdType = fieldType.getParamType(); + if (BaseDataType.class.isAssignableFrom(fwdType) && defaultStrVal != null) + { + if (fwdType == clob.class || fwdType == blob.class) + { + ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName); + // Unable to set initial value '' from XML Schema for field ''. + return false; + } + + try + { + Constructor ctor = + (Constructor) fwdType.getConstructor(String.class); + defVal = ctor.newInstance(defaultStrVal); + } + catch (NoSuchMethodException | IllegalAccessException | + InstantiationException | InvocationTargetException e) + { + ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName); + // Unable to set initial value '' from XML Schema for field ''. + return false; + } + } + + if (toMatch == null) + { + P2JField newField = new P2JField( + fieldName, fieldType, extent, format, defVal, null, null, cs, null, null, + false, null, null, null, null, false, decimals); + ((TempTableBuilder) att).addField(newField); + } + else if (verifySchemaMode != Verify.IGNORE) + { + Property prop = toMatch.byLegacyName(fieldName); + if (prop == null || !prop._ablType.equals(ablType) || prop.extent != extent) + { + if (verifySchemaMode == Verify.STRICT) + { + ErrorManager.recordOrShowError(13201, ablType, fieldName); + return false; + } + } + if (extent != 0 && useLobs && + ("character".equalsIgnoreCase(ablType) || + "raw".equalsIgnoreCase(ablType))) + { + // LOBS (BLOB or CLOB) cannot have extents + ErrorManager.recordOrShowError(13141, fieldName); + // Invalid 'maxOccurs' attribute value set for BLOB or CLOB field ''. + ErrorManager.recordOrShowError(13033); + // Verify Temp-Table or dataset schema against XML Schema failed. + return false; + } + + if (fieldsToMatch != null) // implies [verifySchemaMode == Verify.STRICT] + { + fieldsToMatch.remove(prop); + } + } + } + break; + + case "unique": + // found an unique index + String primary = getXmlAttribute("prodata:primaryIndex"); + if (!readIndex(getXmlAttribute("name"), nsPrefix, Boolean.parseBoolean(primary), true)) + { + return false; + } + break; + + case "index": + // found an non-unique index + if (!readIndex(getXmlAttribute("name"), nsPrefix, false, false)) + { + return false; } break; @@ -1161,6 +1697,13 @@ if ("complexType".equals(name)) { moreFields = false; + if (fieldsToMatch != null && !fieldsToMatch.isEmpty()) // implies [verifySchemaMode == Verify.STRICT] + { + Property fail = fieldsToMatch.iterator().next(); + ErrorManager.recordOrShowError(13090, fail.legacy, name); + // XML Schema definition for field '' in table '
' not found. + return false; + } } if (!moreFields && "element".equals(name)) { @@ -1250,6 +1793,36 @@ } /** + * Skip the whole subtree because the record belongs to an unknown table. Fast forward to the end tag of + * the record. + * + * @param tableName + * The table name. + * + * @return {@code true} on success. Otherwise an error is issued before returning {@code false}. + * + * @throws XMLStreamException + * if the XML stream cannot be parsed. + */ + private boolean skipRecord(String tableName) + throws XMLStreamException + { + while (reader.hasNext()) + { + int type = reader.next(); + if (type == XMLStreamReader.END_ELEMENT) + { + String name = reader.getLocalName(); + if (tableName.equals(name)) + { + return true; // all OK + } + } + } + throw new XMLStreamException("Unexpected EOS in readSchema().", reader.getLocation()); + } + + /** * Read XML content from the stream corresponding with a single data record and store it in * the temp-table. * @@ -1279,6 +1852,7 @@ boolean processNested = false; String name = null; boolean batchError = true; + RecordBuffer recBuffer = ((BufferImpl) proxy).buffer(); try { RecordBuffer.startBatch(true); @@ -1309,7 +1883,7 @@ { after.findByRowID(peer, LockType.EXCLUSIVE); ((TemporaryBuffer) after.buffer()).peerRowid(proxy.rowID()); - ((TemporaryBuffer) ((BufferImpl) proxy).buffer()).peerRowid(after.rowID()); + ((TemporaryBuffer) recBuffer).peerRowid(after.rowID()); after.release(); } else @@ -1326,7 +1900,7 @@ value.equalsIgnoreCase(STR_ROW_DELETED) ? Buffer.ROW_DELETED : value.equalsIgnoreCase(STR_ROW_MODIFIED) ? Buffer.ROW_MODIFIED : Buffer.ROW_UNMODIFIED; - ((TemporaryBuffer) ((BufferImpl) proxy).buffer()).rowState(state); + ((TemporaryBuffer) recBuffer).rowState(state); if (state == Buffer.ROW_CREATED) { // a bit clumsy: we need to create the before image for this kind of record @@ -1337,7 +1911,7 @@ before.create(); ((TemporaryBuffer) before.buffer()).rowState(Buffer.ROW_CREATED); ((TemporaryBuffer) before.buffer()).peerRowid(proxy.rowID()); - ((TemporaryBuffer) ((BufferImpl) proxy).buffer()).peerRowid(before.rowID()); + ((TemporaryBuffer) recBuffer).peerRowid(before.rowID()); before.setUpBeforeBuffer(false); before.release(); } @@ -1369,7 +1943,7 @@ TempTableSchema.Column column = schema.getColumn(name); if (column == null) { - // maybe this is a record of a nested table of teh dataset? + // maybe this is a record of a nested table of the dataset? if (ds != null) { if (ds.bufferHandle(name) != null) @@ -1404,10 +1978,71 @@ batchError = false; } + catch (XMLStreamException xmlEx) + { + ErrorManager.recordOrShowError(13035, source.getFileName(), ""); + // Error reading XML file ''. + return false; + } finally { extentTracker.reset(); + + if (readMode == Read.MERGE || readMode == Read.REPLACE) + { + boolean validated = false; + try + { + validated = recBuffer.validate(false); + } + catch (ValidationException e) + { + // ignore, no 'expected' exception will be thrown + } + + // was there an unique conflict with this record? + if (!validated) + { + if (readMode == Read.MERGE) + { + // merge mode: ignore it, just skip to next record + proxy.release(); // TODO: not enough + } + else + { + // replace mode: eliminate competition, then try again on clean ground + try + { + OrmUtils.dropUniqueIndexConflicts(recBuffer.buffer(), + recBuffer.buffer().getPersistence(), + recBuffer.buffer().getMultiplexID()); + } + catch (PersistenceException e) + { + throw new ErrorConditionException( + "FILL: Failed to make room for new record in REPLACE mode", e); + } + // now there should not be any collisions and the new record should be flushed + } + } + } + + ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper(); + boolean wm = eh.isWarningMode(); + if (!wm) + { + // this is mostly for APPEND mode, the other ones will not encounter issues here + eh.setWarningMode(true); + } RecordBuffer.endBatch(batchError); + if (!wm) + { + eh.setWarningMode(false); + } + if (ErrorManager.isPendingError()) + { + return false; + } } if (processNested) @@ -1615,9 +2250,18 @@ * @param type * The pure {@code xsd} type. * @param proType - * Addtional information needed to distinct the original 4GL type. + * Additional information needed to distinct the original 4GL type. * @param nullable * Whether the original field type was nullable. + * @param useLobs + * If {@code true}, {@code clob} and {@code blob} types will be used instead of {@code character} + * and {@code raw}, respectively. + * @param tableName + * Optional. If specified the table name is looked up in the type map instead of the type + * specified in XML file. + * @param fieldName + * Optional. If specified the field name is looked up in the type map instead of the type + * specified in XML file. * * @return The original 4GL type of the field. If one cannot be detected using the provided * data, {@code null} is returned. @@ -1625,16 +2269,41 @@ * @see com.goldencode.p2j.persist.serial.Util#xsdMap * @see com.goldencode.p2j.persist.serial.Util#xsdProdataMap */ - private static String get4GLType(String type, String proType, String nullable) + private String get4GLType(String type, + String proType, + String nullable, + boolean useLobs, + String tableName, + String fieldName) { if (type == null) { return null; } + + if (fieldTypeMapping != null && fieldName != null) + { + // TODO: is un-scoped field lookup necessary? + String prefType = fieldTypeMapping.get(fieldName); + if (prefType != null) + { + return prefType; + } + + if (tableName != null) + { + prefType = fieldTypeMapping.get(tableName + "." + fieldName); + if (prefType != null) + { + return prefType; + } + } + } + switch (type) { case "xsd:string": - return "character"; + return useLobs || "prodata:clob".equals(proType) ? "clob" : "character"; case "xsd:int": return "integer"; case "xsd:decimal": @@ -1646,7 +2315,11 @@ case "xsd:dateTime": return "prodata:dateTime".equals(proType) ? "datetime" : "datetime-tz"; case "xsd:base64Binary": - return "prodata:rowid".equals(proType) ? "rowid" : "raw"; + if ("prodata:rowid".equals(proType)) + { + return "rowid"; + } + return useLobs || "prodata:blob".equals(proType) ? "blob" : "raw"; case "xsd:long": if ("prodata:recid".equals(proType)) { === modified file 'src/com/goldencode/p2j/persist/serial/XmlTempTableSchema.java' --- src/com/goldencode/p2j/persist/serial/XmlTempTableSchema.java 2019-10-26 18:06:55 +0000 +++ src/com/goldencode/p2j/persist/serial/XmlTempTableSchema.java 2020-11-20 18:40:59 +0000 @@ -1,12 +1,14 @@ /* -** Module : XmlExport.java +** Module : XmlTempTableSchema.java ** Abstract : Write temp-table data into an XML stream. ** -** Copyright (c) 2017-2019, Golden Code Development Corporation. +** Copyright (c) 2017-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 ECF 20190123 Extracted from TempTableSchema. ** 002 OM 20190831 Added XML name support for reading table schema. +** 003 OM 20201120 xmlTableName is not permanent and cannot be cached here. Since this attribute was removed +** this class is subject to deletion and replacement with its super class. */ /* @@ -63,9 +65,7 @@ */ package com.goldencode.p2j.persist.serial; -import java.util.*; import com.goldencode.p2j.persist.*; -import com.goldencode.p2j.util.*; /** * The schema used for serialization of a temp-table to XML. @@ -73,9 +73,6 @@ class XmlTempTableSchema extends TempTableSchema { - /** The name of table XML file. */ - private String xmlTableName = null; - /** * Constructor which gathers table and field data from a temp-table buffer. * @@ -86,39 +83,4 @@ { super(buffer); } - - /** - * Compute the XML node name for this table, from the {@link #serializeOptions}. - * - * @return See above. - */ - public String getTableName() - { - if (xmlTableName == null) - { - if (serializeOptions.getXmlNodeName().length() > 0) - { - xmlTableName = serializeOptions.getXmlNodeName(); - } - else - { - xmlTableName = getName(); - - // TODO: compute from serialize-name, namespace-prefix and namespace-uri - } - } - - return xmlTableName; - } - - /** - * Sets the XML table name when the chame is read from XML file. - * - * @param xmlTableName - * The new XML name for the table. - */ - public void setXmlTableName(String xmlTableName) - { - this.xmlTableName = xmlTableName; - } } === modified file 'src/com/goldencode/p2j/persist/trigger/TriggerTracker.java' --- src/com/goldencode/p2j/persist/trigger/TriggerTracker.java 2020-07-05 23:27:34 +0000 +++ src/com/goldencode/p2j/persist/trigger/TriggerTracker.java 2021-01-04 12:29:06 +0000 @@ -2,7 +2,7 @@ ** Module : TriggerTracker.java ** Abstract : Helper class to manage database trigger information for a single record buffer ** -** Copyright (c) 2014-2020, Golden Code Development Corporation. +** Copyright (c) 2014-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --------------------------------------Description---------------------------------------- ** 001 ECF 20140801 Created initial version. @@ -10,6 +10,8 @@ ** 003 AIL 20180821 Switched pendingBatchFields to LinkedList to be aware of ** multiple firing of the same trigger in the same batch. ** 004 ECF 20200705 Eliminated resetState to enable performance optimization in caller. +** 005 AIL 20201210 Added helper to check if any assign trigger is up for the buffer. +** 20210104 Updated hasAnyAssignTrigger to take in account a predefined set of properties. */ /* @@ -175,28 +177,8 @@ // assign triggers require per-property handling, but can be disabled en masse if (DatabaseEventType.ASSIGN.equals(det)) { - if ((state & checkedMask) == UNCHECKED) - { - if (manager.areAssignTriggersEnabled(bufferClass)) - { - // assign triggers enabled, but we don't yet know which are registered - state |= enabledMask; - assignTriggers = new HashMap<>(); - } - else - { - assignTriggers = null; - } - - // remember we checked enable status for this block - state |= checkedMask; - } - - if ((state & enabledMask) != enabledMask) - { - // all assign triggers are disabled + if (!areAssignTriggersEnabled()) return false; - } Boolean enabled = assignTriggers.get(property); if (enabled == null) @@ -225,6 +207,38 @@ } /** + * Check if there are any assign triggers up for this buffer. + * + * @param props + * The property names of the properties for which the assign triggers should be + * checked. + * + * @return {@code true} if there is any enabled assign trigger. + */ + public boolean hasAnyAssignTrigger(Set props) + { + if (!areAssignTriggersEnabled()) + return false; + + for (String prop : props) + { + Boolean enabled = assignTriggers.get(prop); + if (enabled == null) + { + enabled = manager.isAssignTrigger(bufferClass, prop); + assignTriggers.put(prop, enabled); + } + + if (enabled) + { + return true; + } + } + + return false; + } + + /** * Push a trigger type onto the stack of trigger types currently executing for the record from * the associated record. This should done just before the trigger of this type is fired. * @@ -512,4 +526,34 @@ resetOldDMO(); } } + + /** + * Check if the assign triggers are enabled, as they can be bulk disabled. + * + * @return {@code true} if they aren't bulk disabled. + */ + private boolean areAssignTriggersEnabled() + { + int enabledMask = DatabaseEventType.ASSIGN.getMask(); + int checkedMask = enabledMask << 8; + + if ((state & checkedMask) == UNCHECKED) + { + if (manager.areAssignTriggersEnabled(bufferClass)) + { + // assign triggers enabled, but we don't yet know which are registered + state |= enabledMask; + assignTriggers = new HashMap<>(); + } + else + { + assignTriggers = null; + } + + // remember we checked enable status for this block + state |= checkedMask; + } + + return (state & enabledMask) == enabledMask; + } } === modified file 'src/com/goldencode/p2j/preproc/Environment.java' --- src/com/goldencode/p2j/preproc/Environment.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/preproc/Environment.java 2020-10-15 07:54:57 +0000 @@ -56,6 +56,7 @@ ** detect unbalanced end of include file markers. ** GES 20200629 Made the unbalanced eoi into a warning (an error is too much since this ** condition can be ignored). +** 020 CA 20201015 Replaced java.util.Stack with ArrayDeque (as synchronization is not required). */ /* @@ -1199,7 +1200,7 @@ { int index = 0; List list = hints.getHintsList(); - Stack stack = hints.getHintsStack(); + Deque stack = hints.getHintsStack(); if (label.length() > 0) { === modified file 'src/com/goldencode/p2j/preproc/Hints.java' --- src/com/goldencode/p2j/preproc/Hints.java 2017-08-25 15:21:27 +0000 +++ src/com/goldencode/p2j/preproc/Hints.java 2020-10-15 07:54:57 +0000 @@ -2,7 +2,7 @@ ** Module : Hints.java ** Abstract : Encapsulates elementary hints and corresponding XML tree. ** -** Copyright (c) 2004-2017, Golden Code Development Corporation. +** Copyright (c) 2004-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 NVS 20050513 @21162 Created. Initial implementation. @@ -14,6 +14,7 @@ ** 005 GES 20090515 @42217 Import changes. ** 006 CA 20170825 Enhanced to allow collection and reporting of preprocessor constant ** symbols. +** 007 CA 20201015 Replaced java.util.Stack with ArrayDeque (as synchronization is not required). */ /* ** This program is free software: you can redistribute it and/or modify @@ -94,13 +95,13 @@ private List list = null; /** stack of include hints */ - private Stack includes = null; + private Deque includes = null; /** XML document */ private Document dom = null; /** stack of DOM parents */ - private Stack parents = null; + private Deque parents = null; /** count of essential elements */ private int elementsCount = 0; @@ -120,8 +121,8 @@ IOException { list = new LinkedList(); - includes = new Stack(); - parents = new Stack(); + includes = new ArrayDeque(); + parents = new ArrayDeque(); this.filename = filename; if (filename != null) @@ -157,7 +158,7 @@ * @return * the hints stack */ - public Stack getHintsStack() + public Deque getHintsStack() { return includes; } === modified file 'src/com/goldencode/p2j/reporting/Metafile.java' --- src/com/goldencode/p2j/reporting/Metafile.java 2019-06-18 01:07:56 +0000 +++ src/com/goldencode/p2j/reporting/Metafile.java 2021-01-15 02:45:37 +0000 @@ -2,11 +2,18 @@ ** Module : Metafile.java ** Abstract : Class to provide GUI metafile service. ** -** Copyright (c) 2019, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 EVL 20190514 Created initial version. ** 002 EVL 20190617 Improving line style/width implementation. +** 003 EVL 20210107 Many bug fixing and improvements. +** EVL 20210108 Completed approach for page footer generation. Added more fixes and improvements to +** clean up some cosmetic deviations. +** EVL 20210111 Continue fixup for cosmetic issues. +** EVL 20210113 Resolution for underlined text issues and improvements for text width calculation. +** Several fixes for filled rectangle painting and adding coordinates validity check. +** EVL 20210114 Fix for text height and space between lines mismatches. Improved line thick setup. */ /* ** This program is free software: you can redistribute it and/or modify @@ -77,9 +84,6 @@ public class Metafile implements PdfProvider { - /** Default DPI value for Windows metafile documenet. */ - public static final int DPI_DEFAULT = 96; - /** Left image alignment. */ public static final int IMAGE_ALIGN_LEFT = 0; @@ -117,13 +121,26 @@ private static final int FONT_SIZE_DEFAULT = 12; /** Font name default value for metafile. */ - private static final String FONT_NAME_DEFAULT = "courier new"; + private static final String FONT_NAME_DEFAULT = "Courier"; /** Zoom type default value for metafile. */ private static final String ZOOM_TYPE_DEFAULT = "page"; + /** Optional scaling from PDF coord to metafile. */ + private static final double pdf2MfDpi = 96.0 / (double)Xpr2PdfWorker.DPI_DEFAULT; + /** To convert columns to metafile pixels */ - private static double pixPerColMf = (double)DPI_DEFAULT / (double)XprEntity.CPI_DEFAULT; + private static final double pixPerColMf = (double)Xpr2PdfWorker.DPI_DEFAULT / + (double)XprEntity.CPI_DEFAULT; + + /** Constant vertical shift for object. Text, line, image and rectangle are involved. */ + private static final double OBJ_Y_SHIFT = 0.45; + + /** If X coord is on the lefd edge (0.0), the value must be increased. */ + private static final double EDGE_LINE_X_SHIFT = 0.125; + + /** Vertical lines should be shifted with this value. */ + private static final double VERT_LINE_Y_SHIFT = 0.01; /** The Z-ordered list of the objects recognized inside current file entity, paginated. */ private List> pages = new ArrayList>(); @@ -152,6 +169,9 @@ /** Current font size (default is 12). */ private int fontSize = FONT_SIZE_DEFAULT; + /** Current font size in metric units (default is 12). */ + private double fontSizeMetric = MetafileHelper.pix2MfUnits(fontSize); + /** Flag indicating if the font is currently bold or not. */ private boolean isBold = false; @@ -170,12 +190,6 @@ /** Background color for filled rectangles.*/ private int fillColor = 0xFFFFFFFF; - /** Current X value in new entity. */ - private double xCurr = 0.0; - - /** Current Y value in new entity. */ - private double yCurr = 0.0; - /** Current X value for page number text. */ private double xPageNum = 0.0; @@ -183,16 +197,31 @@ private double yPageNum = 0.0; /** Page number texts. */ - private String[] pageNumberText = new String[10]; + private List pageNumberTextObj = new ArrayList(); /** Left margin value. */ - private double leftMargin = 0.0; + private double leftMargin = 0.5; /** Top margin value. */ private double topMargin = 0.0; + /** Footer reserverd size value. */ + private double footerHeight = 0.0; + /** Extra vertical distance between two sequential rows of the text. */ - private double extraRowDistance = 0.0; + private double extraRowDistance = 0.05125; + + /** Current page width in device independent units. */ + private double diPageWidth = 0.0; + + /** Current page height in device independent units. */ + private double diPageHeight = 0.0; + + /** Current X value in new entity. */ + private double xCurr = 0.0; + + /** Current Y value in new entity. */ + private double yCurr = 0.0; /** Current line thickness value. */ private int lineWidth = 1; @@ -204,10 +233,10 @@ private int textAlignVertCurr = TEXT_ALIGN_TOP; /** Current horizontal image alignment. */ - private int imageAlignHorizCurr = TEXT_ALIGN_LEFT; + private int imageAlignHorizCurr = IMAGE_ALIGN_LEFT; /** Current vertical image alignment. */ - private int imageAlignVertCurr = TEXT_ALIGN_TOP; + private int imageAlignVertCurr = IMAGE_ALIGN_TOP; /** Flag indicating if the footer will be shown or not. */ private boolean showFooter = false; @@ -215,6 +244,12 @@ /** Current line style value. */ private LineStyleEnum lineStyle = LineStyleEnum.SOLID; + /** Flag indicating there are possibly records to commit. */ + private boolean recordingStopped = false; + + /** Temporary directory to store the intermediate files. */ + private String pdfDir = null; + /** * Default metafile object constructor. */ @@ -223,9 +258,7 @@ // and create new object room for next page objects objects = new ArrayList(); // new page options set with current default page size - pageOptions.add(new XprPageOptions(XprHelper.getPdfPageWidth(), - XprHelper.getPdfPageHeight(), - pageOrientation)); + setPageOptions(); } /** @@ -336,7 +369,10 @@ public void startNewPage() { // add objects of the current page to the page list - pages.add(objects); + if (currPageNum > 1) + { + pages.add(objects); + } // and create new object room for next page objects objects = new ArrayList(); // start recording new page @@ -346,10 +382,8 @@ { maxPageNum++; } - // new page options set with current default page size - pageOptions.add(new XprPageOptions(XprHelper.getPdfPageWidth(), - XprHelper.getPdfPageHeight(), - pageOrientation)); + // new page options set with current default page size + setPageOptions(); } /** @@ -369,11 +403,32 @@ return; } - // normalize to the lower case - fontName = fontName.toLowerCase(); + // normalize the font names to use with PDF font mapper + if (fontName.startsWith("Times ")) + { + currFontName = "Times"; + } + else if (fontName.startsWith("Courier ") || fontName.equals("default")) + { + currFontName = "Courier"; + } + else + { + currFontName = fontName; + } } /** + * Resets new value for text style to be off. + */ + public void resetTextStyle() + { + isItalic = false; + isBold = false; + isUnderline = false; + } + + /** * Gets the current font value. * * @param fontName @@ -419,7 +474,7 @@ */ public void getFontHeight(decimal fontHeight) { - fontHeight.assign(new decimal(fontSize)); + fontHeight.assign(new decimal(fontSizeMetric)); } /** @@ -429,7 +484,7 @@ */ public double getFontHeight() { - return fontSize; + return fontSizeMetric; } /** @@ -440,7 +495,8 @@ */ public void setFontHeight(int fontHeight) { - fontSize = fontHeight; + fontSize = (int)Math.ceil((double)fontHeight / pdf2MfDpi); + fontSizeMetric = MetafileHelper.pix2MfUnits(fontSize); } /** @@ -468,19 +524,51 @@ */ public double getTextWidth(String textToMeasure) { + return getTextWidth(textToMeasure, -1, isBold, isItalic); + } + + /** + * Gets the given text width value. Custom font size is taking into account. + * + * @param textToMeasure + * The text to calculate width. + * @param customFontSize + * The optional font size to use instead of current one. + * @param bold + * The bold font in use to measure text. + * @param italic + * The italic font in use to measure text. + * + * @return The width of the text in metric units. + */ + public double getTextWidth(String textToMeasure, int customFontSize, boolean bold, boolean italic) + { double res = 0.0; + boolean customFontUsage = false; // TODO: this can be inaccurate calculaton, might need to be re-worked if (textToMeasure != null && !textToMeasure.isEmpty()) { + if (customFontSize > 0) + { + customFontSize = (int)Math.round((double)customFontSize / pdf2MfDpi); + customFontUsage = true; + } + else + { + customFontSize = fontSize; + } + int length = textToMeasure.length(); res = MetafileHelper.pix2MfUnits((int)Math.round((double)length * pixPerColMf * - (double)fontSize / - (double)FONT_SIZE_DEFAULT)); + (double)customFontSize / + (double)(FONT_SIZE_DEFAULT))); } - return res; + return customFontUsage ? res * 0.95 : res * MetafileHelper.getTextWeightScale(textToMeasure, + currFontName); } + /** * Sets the new current position for cursor. Can be used a starting point for text/line/image * draw. @@ -528,7 +616,7 @@ */ public void startNewTextLine() { - xCurr = leftMargin; + xCurr = 0.0; yCurr += getFontHeight() + extraRowDistance; } @@ -551,8 +639,7 @@ */ public void getFreeSpaceDown(decimal freeSpaceDown) { - double val = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getHeightPix()) - - yCurr - topMargin; + double val = diPageHeight - yCurr - footerHeight; freeSpaceDown.assign(new decimal(val)); } @@ -564,8 +651,7 @@ */ public void getFreeSpaceRight(decimal freeSpaceRight) { - double val = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getWidthPix()) - - xCurr - leftMargin; + double val = diPageWidth - xCurr; freeSpaceRight.assign(new decimal(val)); } @@ -577,11 +663,25 @@ */ public void drawText(String text) { - XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic, - textColor, MetafileHelper.y2Row(yCurr), - MetafileHelper.x2Col(xCurr), text); - xprText.setMargins(leftMargin, topMargin); - objects.add(xprText); + double x = xCurr; + // consider current alignment and adjust X position + switch (textAlignHorizCurr) + { + case TEXT_ALIGN_RIGHT: + { + x -= getTextWidth(text); + // shift the current X position to the end of the text + break; + } + case TEXT_ALIGN_CENTER: + { + x -= getTextWidth(text) / 2.0; + // shift the current X position to the end of the text + break; + } + } + + addTextObject(text, x, yCurr); // shift the current X position to the end of the text xCurr += getTextWidth(text); } @@ -657,11 +757,7 @@ } } - XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic, - textColor, MetafileHelper.y2Row(yCurr), - MetafileHelper.x2Col(x), text); - xprText.setMargins(leftMargin, topMargin); - objects.add(xprText); + addTextObject(text, x, yCurr); } /** @@ -703,7 +799,7 @@ double boundWidth = right - left; for (int i = text.length() - 1; i >= 0; i--) { - String textMatch = text.substring(0, i); + String textMatch = text.substring(0, i + 1); if (getTextWidth(textMatch) <= boundWidth) { text = textMatch; @@ -732,15 +828,11 @@ } } - XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic, - textColor, MetafileHelper.y2Row(yCurr), - MetafileHelper.x2Col(left), text); - xprText.setMargins(leftMargin, topMargin); - objects.add(xprText); + addTextObject(text, left, yCurr); // shift the current X position to the end of the text xCurr = right; } - + /** * Sets new values for line attributes. * @@ -759,13 +851,9 @@ public void setLineAttributes(String style, double thickness, int redColor, int greenColor, int blueColor) { - // simple protection from empty style value - if (style != null && !style.isEmpty()) - { - lineStyle = getLineType(style.toLowerCase()); - } - - lineWidth = MetafileHelper.mfUnits2pix(thickness); + // line style and thick + setLineStyle(style, thickness); + // and color lineColor = (redColor & 0x000000FF) | (greenColor & 0x000000FF) << 8 | (blueColor & 0x000000FF) << 16; } @@ -798,7 +886,14 @@ lineStyle = getLineType(style.toLowerCase()); } - lineWidth = MetafileHelper.mfUnits2pix(thickness); + if (thickness == 0.0) + { + lineWidth = 1; + } + else + { + lineWidth = MetafileHelper.mfUnits2pix(thickness) + 1; + } } /** @@ -827,9 +922,41 @@ */ public void drawLine(double left, double top, double right, double bottom) { - XprObjLine xprLine = new XprObjLine(lineColor, MetafileHelper.y2Row(top), + // coordinates check + if (top > bottom) + { + // need to have bottom >= top condition + double swapY = top; + top = bottom; + bottom = swapY; + } + if (left > right) + { + // need to have bottom >= top condition + double swapX = left; + left = right; + right = swapX; + } + + // make some adjustments for linex with left edge X coord + if (left == 0.0) + { + left = EDGE_LINE_X_SHIFT; + } + if (right == 0.0) + { + right = EDGE_LINE_X_SHIFT; + } + // and some Y adjust to the all vertical lines + if (left == right) + { + top += VERT_LINE_Y_SHIFT; + bottom += VERT_LINE_Y_SHIFT; + } + + XprObjLine xprLine = new XprObjLine(lineColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT), MetafileHelper.x2Col(left), - MetafileHelper.y2Row(bottom), + MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT), MetafileHelper.x2Col(right), lineWidth, lineStyle); xprLine.setMargins(leftMargin, topMargin); objects.add(xprLine); @@ -850,9 +977,25 @@ */ public void drawRect(double left, double top, double right, double bottom) { - XprObjRectangle xprRect = new XprObjRectangle(lineColor, MetafileHelper.y2Row(top), + // coordinates check + if (top > bottom) + { + // need to have bottom >= top condition + double swapY = top; + top = bottom; + bottom = swapY; + } + if (left > right) + { + // need to have bottom >= top condition + double swapX = left; + left = right; + right = swapX; + } + + XprObjRectangle xprRect = new XprObjRectangle(lineColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT), MetafileHelper.x2Col(left), - MetafileHelper.y2Row(bottom), + MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT), MetafileHelper.x2Col(right), lineWidth, false, lineStyle); xprRect.setMargins(leftMargin, topMargin); @@ -874,9 +1017,32 @@ */ public void drawFilledRect(double left, double top, double right, double bottom) { + // coordinates check + if (top > bottom) + { + // need to have bottom >= top condition + double swapY = top; + top = bottom; + bottom = swapY; + } + if (left > right) + { + // need to have bottom >= top condition + double swapX = left; + left = right; + right = swapX; + } + + // small boxes need additional adjust + if (bottom - top < 2.0 * (getFontHeight() + extraRowDistance)) + { + bottom += 0.03; + } + // filled rectangle needs special attention because the border should not be painted XprObjFilledRectangle xprFillRect = - new XprObjFilledRectangle(lineColor, fillColor, MetafileHelper.y2Row(top), - MetafileHelper.x2Col(left), MetafileHelper.y2Row(bottom), + new XprObjFilledRectangle(fillColor, fillColor, MetafileHelper.y2Row(top + OBJ_Y_SHIFT + 0.04), + MetafileHelper.x2Col(left + 0.01), + MetafileHelper.y2Row(bottom + OBJ_Y_SHIFT), MetafileHelper.x2Col(right), lineWidth, false, lineStyle); xprFillRect.setMargins(leftMargin, topMargin); objects.add(xprFillRect); @@ -960,12 +1126,12 @@ // consider current alignment and adjust X and Y positions switch (imageAlignHorizCurr) { - case TEXT_ALIGN_RIGHT: + case IMAGE_ALIGN_RIGHT: { x -= width; break; } - case TEXT_ALIGN_CENTER: + case IMAGE_ALIGN_CENTER: { x -= width / 2.0; break; @@ -973,12 +1139,12 @@ } switch (imageAlignVertCurr) { - case TEXT_ALIGN_RIGHT: + case IMAGE_ALIGN_BOTTOM: { y -= height; break; } - case TEXT_ALIGN_CENTER: + case IMAGE_ALIGN_CENTER: { x -= height / 2.0; break; @@ -986,6 +1152,8 @@ } // set up image object + x += 0.15; + y += 0.15 + OBJ_Y_SHIFT; XprObjImage xprImage = new XprObjImage(MetafileHelper.y2Row(y), MetafileHelper.x2Col(x), MetafileHelper.y2Row(y + height), MetafileHelper.x2Col(x + width), filename); @@ -1049,6 +1217,8 @@ return; } + // normalize oriantation name + mode = mode.toLowerCase(); // update page orientation if (mode.equals("standard") || mode.equals("portrait")) { @@ -1068,9 +1238,7 @@ */ public void getPageWidth(decimal pageWidth) { - double val = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getWidthPix()) - - 2.0 * leftMargin; - pageWidth.assign(new decimal(val)); + pageWidth.assign(new decimal(diPageWidth)); } /** @@ -1081,9 +1249,7 @@ */ public void getPageHeight(decimal pageHeight) { - double val = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getHeightPix()) - - 2.0 * topMargin; - pageHeight.assign(new decimal(val)); + pageHeight.assign(new decimal(diPageHeight - footerHeight)); } /** @@ -1153,7 +1319,7 @@ public void setPageNumberPosition(double x, double y) { xPageNum = x; - yPageNum = y; + yPageNum = y + 0.2; } /** @@ -1166,7 +1332,11 @@ */ public void setPageNumberText(String text, int pgNumberNdx) { - pageNumberText[pgNumberNdx-1] = new String(text); + // the X and Y positions will be set later + XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic, + textColor, 0.0, 0.0, text); + xprText.setMargins(leftMargin, topMargin); + pageNumberTextObj.add(xprText); } /** @@ -1192,8 +1362,99 @@ */ public void initialize(double footerTextHeight, String tmpDir, integer ok) { + // reserve several lines for page footer + footerHeight = 1.5 * footerTextHeight; + pdfDir = tmpDir; ok.assign(new integer(1)); } + + /** + * Stops all operation for the current metafile and commit remaining objects to the current page. + */ + public void stopRecording() + { + // already stopped current recording + if (recordingStopped) + { + return; + } + + // commit remaining objects + if (objects.size() > 0) + { + pages.add(objects); + } + + // now add the optional page number texts + int pageNumberObjSize = pageNumberTextObj.size(); + if (pageNumberObjSize > 0) + { + // prepare total pages string value to put into every page + int pagesTotalInt = pages.size(); + String pagesTotalStr = String.valueOf(pagesTotalInt); + // modify total pages number object + XprObjText xprTextTotal = pageNumberTextObj.get(2); + if (xprTextTotal != null) + { + xprTextTotal.appendText(pagesTotalStr); + } + // need to walk through all pages + for (int i = 0; i < pagesTotalInt; i++) + { + List pageCurr = getObjectsInPage(i); + double xPageNumCurrent = xPageNum; + // now enumerate all page number texts in reverse order and finalize object preparation + for (int j = pageNumberObjSize - 2; j >= 0; j--) + { + XprObjText xprOldText = pageNumberTextObj.get(j); + int customFontSize = xprOldText.getFontSize(); + XprObjText xprNewText = null; + String newFooterText = null; + switch (j) + { + case 1: + { + // need to crate new text object for every page in the document, + // because this part of the text is different for each page + newFooterText = xprOldText.getValue() + String.valueOf(i + 1) + + (xprTextTotal != null ? xprTextTotal.getValue() : ""); + break; + } + case 0: + { + newFooterText = xprOldText.getValue(); + break; + } + } + if (newFooterText != null) + { + xPageNumCurrent -= getTextWidth(newFooterText, customFontSize, + xprOldText.isBold(), xprOldText.isItalic()); + xprNewText = new XprObjText(xprOldText.getFontName(), customFontSize, + xprOldText.isBold(), xprOldText.isItalic(), + xprOldText.getColor(), MetafileHelper.y2Row(yPageNum), + MetafileHelper.x2Col(xPageNumCurrent), newFooterText); + pageCurr.add(xprNewText); + } + } + } + } + // clean up page number list + pageNumberTextObj.clear(); + + // all done + recordingStopped = true; + } + + /** + * Gets optionally set temporary directory to store generated PDF temporary files. + * + * @return The currently used temporary directory. + */ + public String getTmpDirectory() + { + return pdfDir; + } /** * Converts line style string into JasperReports compatible line type. @@ -1220,6 +1481,65 @@ return lsRet; } + /** + * Set up options for new current document page. + */ + private void setPageOptions() + { + pageOptions.add(new XprPageOptions(XprHelper.getPdfPageWidth(), XprHelper.getPdfPageHeight(), + pageOrientation)); + diPageWidth = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getWidthPix()); + diPageHeight = MetafileHelper.pix2MfUnits(pageOptions.get(currPageNum - 1).getHeightPix()); + + } + + /** + * Internal method to construct and add new text object. + * + * @param textToAdd + * The line of the text to be added.. + * @param xText + * The X text position. + * @param yText + * The Y text position. + */ + private void addTextObject(String textToAdd, double xText, double yText) + { + xText += 0.1; + // consider current alignment and adjust Y position + switch (textAlignVertCurr) + { + case TEXT_ALIGN_BASELINE: + { + yText -= 0.125 + fontSizeMetric / 2.0; + // shift the current X position to the end of the text + break; + } + case TEXT_ALIGN_BOTTOM: + { + yText -= fontSizeMetric; + // shift the current X position to the end of the text + break; + } + } + + XprObjText xprText = new XprObjText(currFontName, fontSize, isBold, isItalic, + textColor, MetafileHelper.y2Row(yText + OBJ_Y_SHIFT), + MetafileHelper.x2Col(xText), textToAdd); + xprText.setMargins(leftMargin, topMargin); + objects.add(xprText); + // simulate underlined text with additional line when necessary + if (isUnderline) + { + yText += getFontHeight(); + // current line width may be other than 1 we need to keep this value untouched + int savedLineWidth = lineWidth; + lineWidth = 2; + drawLine(xText - 0.01, yText, xText + getTextWidth(textToAdd) - 0.04, yText); + lineWidth = savedLineWidth; + } + } + //-------------------- Implementation of the PdfProvoder interface --------------------------- /** * Getting internal object list from XPR object for particular page. === modified file 'src/com/goldencode/p2j/reporting/MetafileHelper.java' --- src/com/goldencode/p2j/reporting/MetafileHelper.java 2019-06-19 01:10:28 +0000 +++ src/com/goldencode/p2j/reporting/MetafileHelper.java 2021-01-15 03:10:10 +0000 @@ -2,11 +2,16 @@ ** Module : MetafileHelper.java ** Abstract : Helper class to provide report generation from generic GUI metafile. ** -** Copyright (c) 2019, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 EVL 20190510 Created initial version. ** 002 EVL 20190618 Removed printDoc and sendOptionalText from metafile extension set. +** 003 EVL 20201216 Added more clean up for metafile stop/interrupt processing. +** EVL 20210106 Added code to properly finalize the metafile recording. +** EVL 20210107 Added new helper methods to get current temporary directory and reset text styles. +** EVL 20210113 Added new helper method to calculate text scaling factor. +** EVL 20210114 Changed to use common constant from Xpr2PdfWorker. */ /* ** This program is free software: you can redistribute it and/or modify @@ -174,6 +179,14 @@ } /** + * Resets new value for text style to be off. + */ + public static void resetTextStyle() + { + resolveCurrentMetafile().resetTextStyle(); + } + + /** * Starts the new page construction for the document that is currently processing. */ public static void startNewPage() @@ -1470,6 +1483,16 @@ } /** + * Gets optionally set temporary directory to store generated PDF temporary files. + * + * @return The currently used temporary directory. + */ + public static String getTmpDirectory() + { + return resolveCurrentMetafile().getTmpDirectory(); + } + + /** * Makes PDF file from the currently recorded metafile. * * @param pdfFileName @@ -1483,6 +1506,9 @@ // intercept Jasper related issues tp avoid client abend too try { + // ensure all pending objects are committed + wa.currentMf.stopRecording(); + // time to create jasper related worker Xpr2PdfWorker pdfOutput = new Xpr2PdfWorker(wa.currentMf, pdfFileName); // export to PDF @@ -1526,6 +1552,15 @@ */ public static void stopRecording() { + resolveCurrentMetafile().stopRecording(); + } + + /** + * Interrupts all operation for the current metafile. + */ + public static void interruptRecording() + { + work.obtain().currentMf = null; } /** @@ -1598,7 +1633,7 @@ public static double pix2MfUnits(int pixelValue) { // defaulting to inches - double res = (double)pixelValue / (double) (Metafile.DPI_DEFAULT); + double res = (double)pixelValue / (double) (Xpr2PdfWorker.DPI_DEFAULT); // can be CM or MM as well if (units == UNITS_MM) @@ -1624,7 +1659,7 @@ public static int mfUnits2pix(double mfValue) { // defaulting to use inches - double res = (double)mfValue * (double) (Metafile.DPI_DEFAULT); + double res = (double)mfValue * (double) (Xpr2PdfWorker.DPI_DEFAULT); // can be CM or MM as well if (units == UNITS_MM) @@ -1651,6 +1686,79 @@ { return op != null && !op.isUnknown(); } + + /** + * Calculates the scaling factor for given text. Different letters have different width for variable + * size fonts like Arial. We need to compensate this difference to get proper text width. + * + * @param textToTest + * The text to measure letters scale. + * @param fontName + * The font to be used to draw the text. + * + * @return The scaling for given string to calculate text width. + */ + public static double getTextWeightScale(String textToTest, String fontName) + { + double res = 0.0; + + // some simple NPE and errors protection + // fixed width font can also be exculded + if (textToTest == null || textToTest.isEmpty() || fontName.equals("Courier")) + { + return 1.0; + } + + // text length + int length = textToTest.length(); + for (int i = 0; i < length; i++) + { + char chNext = textToTest.charAt(i); + switch (chNext) + { + case 'I': + case 'i': + case 'L': + case 'l': + case ' ': + case '.': + case ',': + case '/': + case '-': + case '*': + { + // narrow letters + res += 0.75; + break; + } + case 'W': + case 'w': + case 'M': + case 'm': + case 'G': + case 'E': + case 'A': + case 'R': + case 'O': + case 'D': + { + // wide letters + res += 1.25; + break; + } + default: + { + // all other letters are regular + res += 1.0; + break; + } + } + } + // normalize the scale factor + res /= (double)length; + + return res; + } /** * Gets the instance of the current metafile to process. === modified file 'src/com/goldencode/p2j/reporting/XprObjBase.java' --- src/com/goldencode/p2j/reporting/XprObjBase.java 2018-06-05 21:54:04 +0000 +++ src/com/goldencode/p2j/reporting/XprObjBase.java 2021-01-08 04:40:56 +0000 @@ -2,13 +2,14 @@ ** Module : XprObjBase.java ** Abstract : Class to represent base object feature for XPR content elements. ** -** Copyright (c) 2018, Golden Code Development Corporation. +** Copyright (c) 2018-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 EVL 20180510 Created initial version. ** 002 EVL 20180530 Adding left margin field support. ** 003 EVL 20180602 Minor javadoc fix. Changed default color to full non-transparent. Adding ** support for top margin tag. +** 004 EVL 20210107 Adding new method to change object position after creation. */ /* ** This program is free software: you can redistribute it and/or modify @@ -202,6 +203,20 @@ } /** + * Sets the object new position coordinates. + * + * @param row + * The new value for text row. + * @param column + * The new value for text column. + */ + public void setPosition(double row, double column) + { + rowStart = row; + colStart = column; + } + + /** * Returns the object type value. * * @return The type of the given object. === modified file 'src/com/goldencode/p2j/schema/ImportWorker.java' --- src/com/goldencode/p2j/schema/ImportWorker.java 2020-09-25 16:00:36 +0000 +++ src/com/goldencode/p2j/schema/ImportWorker.java 2021-01-29 19:50:19 +0000 @@ -142,6 +142,8 @@ ** 051 OM 20191120 Transaction.rollback throws exception. ** OM 20200507 Upgraded to new persistence framework, without Hibernate. ** 052 OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them. +** 053 IAS 20201219 Added 'CPSTREAM' use on import +** Added word tables population on import */ /* @@ -206,14 +208,20 @@ import java.sql.Statement; import java.util.*; import java.util.logging.*; +import java.util.stream.*; + +import org.apache.commons.lang3.*; +import org.apache.log4j.lf5.*; + import com.goldencode.p2j.cfg.*; -import com.goldencode.p2j.util.ErrorManager; import com.goldencode.p2j.pattern.*; import com.goldencode.p2j.persist.*; +import com.goldencode.p2j.persist.dialect.*; import com.goldencode.p2j.persist.orm.*; import com.goldencode.p2j.persist.orm.types.*; -import com.goldencode.p2j.persist.dialect.*; +import com.goldencode.p2j.persist.pl.*; import com.goldencode.p2j.util.*; +import com.goldencode.p2j.util.ErrorManager; import com.mchange.v2.c3p0.*; /** @@ -840,6 +848,8 @@ /** * Sets the values of the sequences. All sequences must be already added to the list. * + * @param path + * Path to the file file to which the sequences' values have been exported * @param seqDumpFile * The file to which the sequences' values have been exported * @@ -986,8 +996,8 @@ } /** - * Import all data for a table into the database, using the given record - * loader and current session factory. Uses the {@link Stream#readField} + * Import all data for a table into the database, using the given record loader + * and current session factory. Uses the {@link com.goldencode.p2j.util.Stream#readField} * to scan the input file. Each record's data is stored in a new * instance of a data model object (DMO). Hibernate is used to persist * the DMO to the backing database. @@ -1053,6 +1063,11 @@ try { + List wordTables = indexes.stream().filter(P2JIndex::isWord). + filter(index -> index.components().hasNext()). + map(idx -> new WordTableInfo(loader, idx)). + filter(wt -> wt.isValid). + collect(Collectors.toList()); // Attempt to drop indexes. List nonRedundantIndexes = P2JIndex.nonRedundantIndexes(indexes); dropIndexes(nonRedundantIndexes); @@ -1171,9 +1186,11 @@ nextID = idCtx.getNextID(); dmo.primaryKey(nextID); session.save(dmo, false); - // flush insert and release memory session.clear(); + + populateWordTables(session, dmo, wordTables); + // commit transaction if (inTx) @@ -1198,10 +1215,15 @@ // insert records using SQL batching session.bulkSave(records); - // release memory // TODO: implement cache-less session and drop this session.clear(); + + for (BaseRecord dmo : records) + { + populateWordTables(session, dmo, wordTables); + } + // commit transaction session.commit(); @@ -1322,7 +1344,7 @@ { int processed = adjustedCounter(counter, badIndex, dropCount); int lost = counter % batchSize; - long recCount = stream.getRecordCount(); + long recCount = stream == null ? - 1 : stream.getRecordCount(); LOG.log(Level.SEVERE, "Error processing import data from " + dataFile + "; " + processed + " of " + (recCount < 0 ? "?" : recCount) + " record(s) successfully processed; " + @@ -1360,6 +1382,155 @@ } /** + * Populate word tables with data from the record. + * + * @param session + * Database session. + * @param dmo + * Master record. + * @param wordTables + * word tables' data + * @throws PersistenceException + * @throws SQLException + */ + private void populateWordTables(Session session, BaseRecord dmo, + List wordTables) throws PersistenceException, SQLException + { + if (wordTables.isEmpty()) + { + return; + } + Object[] data = dmo.getData(wordTables.get(0).mapper); + if (data == null) + { + return; + } + Connection conn = session.getConnection(); + for (WordTableInfo wordTable: wordTables) + { + if (wordTable.extent == 0) + { + String value = (String) data[wordTable.offset]; + // NB: uppercase at the database side + String[] words = Functions.words(value, false); + populateWordTable(conn, wordTable.wordTableName, dmo.primaryKey(), + words, !wordTable.caseSensitive); + } + else + { + for (int i = 0; i < wordTable.extent; i++) + { + String value = (String) data[wordTable.offset + i]; + // NB: uppercase at the database side + String[] words = Functions.words(value, false); + populateWordTable(conn, wordTable.wordTableName, dmo.primaryKey(), i, + words, !wordTable.caseSensitive); + } + } + } + } + + /** + * Populate word tables with data. + * + * @param conn + * Database connection. + * @param wordTableName + * word table name. + * @param primaryKey + * primary key of the master record + * @param words + * words in the field + * @param toUppercase + * convert words to uppercase + * @throws SQLException + */ + private void populateWordTable(Connection conn, String wordTableName, Long primaryKey, + String[] words, boolean toUppercase) throws SQLException + { + if (words == null || words.length == 0) + { + return; + } + StringBuilder sql = new StringBuilder("INSERT INTO "). + append(wordTableName).append(" VALUES"); + for (int i = 0; i < words.length; i++) + { + // NB: uppercase at the database side + sql.append(i == 0 ? "" : ",").append(toUppercase ? "(?,UPPER(?))" : "(?,?)"); + } + Savepoint sp = conn.setSavepoint(); + try(PreparedStatement pstmt = conn.prepareStatement(sql.toString())) + { + int i = 1; + for(String word: words) + { + pstmt.setLong(i++, primaryKey); + pstmt.setString(i++, word); + } + pstmt.executeUpdate(); + } + catch(SQLException e) + { + LOG.log(Level.WARNING, "Failed to populate word table: " + wordTableName + + " pk = " + primaryKey, e); + conn.rollback(sp); + } + } + + /** + * Populate word tables with data. + * + * @param conn + * Database connection. + * @param wordTableName + * word table name. + * @param primaryKey + * primary key of the master record + * @param index + * index in the extent + * @param words + * words in the field + * @param toUppercase + * convert words to uppercase + * @throws SQLException + */ + private void populateWordTable(Connection conn, String wordTableName, + Long primaryKey, int index, + String[] words, boolean toUppercase) throws SQLException + { + if (words == null || words.length == 0) + { + return; + } + StringBuilder sql = new StringBuilder("INSERT INTO "). + append(wordTableName).append(" VALUES"); + for (int i = 0; i < words.length; i++) + { + // NB: uppercase at the database side + sql.append(i == 0 ? "" : ",").append(toUppercase ? "(?,?,UPPER(?))" : "(?,?,?)"); + } + Savepoint sp = conn.setSavepoint(); + try(PreparedStatement pstmt = conn.prepareStatement(sql.toString())) + { + int i = 1; + for(String word: words) + { + pstmt.setLong(i++, primaryKey); + pstmt.setInt(i++, index); + pstmt.setString(i++, word); + } + pstmt.executeUpdate(); + } + catch(SQLException e) + { + LOG.log(Level.WARNING, "Failed to populate word table: " + wordTableName + + " pk = " + primaryKey + "index = " + index, e); + conn.rollback(sp); + } + } + + /** * Generate the DDL to drop and recreate the list of indexes for the given table. Indexes on * {@code table} which are not within the list are ignored. Also, the redundant indexes are * filtered out so only the minimum list of required indexes is written to the given output @@ -1375,6 +1546,7 @@ * Table which is to be reindexed. * @param indexes * List of index definitions. + * @throws IOException */ public void generateIndexDDL(String dialectName, String filename, @@ -1420,8 +1592,6 @@ * @param indexes * List of index definitions. * - * @throws SQLException - * if any error occurs opening or closing JDBC statements. * @throws PersistenceException * if any error occurs opening or closing sessions, or obtaining database * connections. @@ -1462,6 +1632,19 @@ } /** + * Get word table name for the DMO and index name + * @param ifaceName + * DMP interface name + * @param indexName + * index name + * @return word table name + */ + public String getWordTableName(String ifaceName, String indexName) + { + DmoMeta meta = DmoMetadataManager.getDmoInfo(ifaceName); + return meta.getWordTablesByIndexName().get(indexName); + } + /** * Return the next import bundle which is eligible for import. A bundle is eligible for * import if all of the tables upon which it is dependent already have been loaded. Bundles * are returned from this method in the order in which they were ranked, except that a @@ -2364,6 +2547,7 @@ /** The date format as it was set when the table was dumped. */ private String dateFormat = null; + /** * Constructor. * @@ -2384,7 +2568,6 @@ { processPscHeader(); } - // read only files shift directly into memory mapped mode at the start of the file map(0L); } @@ -2550,6 +2733,7 @@ } catch (IOException | InterruptedException exc) { + exc.printStackTrace(); // ignore } @@ -2614,4 +2798,60 @@ } } } + + /** + * Auxiliary data for the word table + */ + private static class WordTableInfo + { + /** Word table name */ + public final String wordTableName; + /** field name */ + public final String fieldName; + /** flag indication that the field is case-sensitive */ + public final boolean caseSensitive; + /** PropertyMapper for the DMO record property */ + public final PropertyMapper mapper; + public final int extent; + public final int offset; + /** Flag indicating that data is valid */ + public final boolean isValid; + + /** + * Constructor + * @param loader + * record loader + * @param index + * word index descriptor + */ + public WordTableInfo(RecordLoader loader, P2JIndex index) + { + boolean ok = true; + P2JIndexComponent indexComponent = index.components().next(); + this.wordTableName = index.getWordTableName(); + if (StringUtils.isEmpty(wordTableName)) + { + LOG.log(Level.WARNING, "wordTableName is empty for index: [" + + index.getTable() + "." + index.getName() + "]"); + ok = false; + } + this.fieldName = indexComponent.getColumnName(); + this.caseSensitive = !indexComponent.isIgnoreCase(); + this.mapper = loader.getPropertyMapperByColumn(fieldName); + if (mapper == null) + { + LOG.log(Level.WARNING, "No PropertyMapper for [" + + index.getTable() + "." + fieldName + "]"); + extent = -1; + offset = -1; + ok = false; + } + else + { + extent = mapper.getPropertyMeta().getExtent(); + offset = mapper.getIndex(); + } + isValid = ok; + } + } } === modified file 'src/com/goldencode/p2j/schema/P2OAccessWorker.java' --- src/com/goldencode/p2j/schema/P2OAccessWorker.java 2020-10-08 20:26:22 +0000 +++ src/com/goldencode/p2j/schema/P2OAccessWorker.java 2021-01-21 22:49:45 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2005-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- ---------------------------- Description ---------------------------- +** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ---------------------------------- ** 001 ECF 20050921 @22771 Created initial version. Lazily loads P2O ** ASTs and enables lookups of Java names for ** legacy Progress table and field names. @@ -58,7 +58,9 @@ ** 021 CA 20200122 Javadoc fixes. ** 022 CA 20200412 Added incremental conversion support. ** 023 CA 20201008 Added APIs to obtain an iterator over the indexes or properties of a table. +** OM 20201231 Avoid java name conflicts. */ + /* ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU Affero General Public License as @@ -148,10 +150,10 @@ types.add(ProgressParserTokenTypes.TEMP_TABLE); types.add(ProgressParserTokenTypes.WORK_TABLE); types.add(ProgressParserTokenTypes.BUFFER); - }; + } /** List of objects describing potentially suspicious query index selection */ - private List suspiciousQueries = new ArrayList<>(); + private final List suspiciousQueries = new ArrayList<>(); /** Source file name whose AST currently is being visited */ private String sourceFile = null; @@ -203,9 +205,7 @@ throw new SchemaException("Unexpected token: " + ast.toString()); } - String table = (String) child.getAnnotation("schemaname"); - - return table; + return (String) child.getAnnotation("schemaname"); } /** @@ -382,7 +382,7 @@ public String javaPropertyName(String historical, boolean qualified) throws SchemaException { - return P2OLookup.javaPropertyName(historical, qualified); + return P2OLookup.javaPropertyName(historical, qualified, null); } /** @@ -414,7 +414,7 @@ } /** - * Retrieve the full set of fully quallified, legacy field names for a + * Retrieve the full set of fully qualified, legacy field names for a * legacy Progress table. The set is guaranteed to iterate in the * default display order defined for the original, Progress schema for * that table. @@ -431,7 +431,7 @@ * if there is any error loading the AST from persistence; * if historical does not represent a known table. */ - public Set legacyFieldNames(String historical, boolean includeHidden) + public Set legacyFieldNames(String historical, boolean includeHidden) throws SchemaException { return P2OLookup.legacyFieldNames(historical, includeHidden); === modified file 'src/com/goldencode/p2j/schema/P2OLookup.java' --- src/com/goldencode/p2j/schema/P2OLookup.java 2020-10-08 20:26:22 +0000 +++ src/com/goldencode/p2j/schema/P2OLookup.java 2021-01-21 22:49:45 +0000 @@ -2,7 +2,7 @@ ** Module : P2OLookup.java ** Abstract : Helper class which loads P2O ASTs and enables lookup access ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ---------------------------------- ** 001 ECF 20051013 @23034 Created initial version. Lazily loads P2O @@ -65,8 +65,7 @@ ** 024 OM 20131030 Added support for checking if a schema contains a certain dmo iface. ** 025 VMN 20140328 Added support for denormalized fields with extent. ** 026 SVL 20140329 loadAst loads data from a resource in runtime mode. -** 027 OM 20140424 Added CASE_SENSITIVE constant used for dynamic temp-tables -** generation. +** 027 OM 20140424 Added CASE_SENSITIVE constant used for dynamic temp-tables generation. ** 028 ECF 20140921 Memory optimization: intern many duplicated strings. ** 029 OM 20150212 Forced lowercase legacy access for javaName accessors while saving ** the original legacy names for later extraction. @@ -91,6 +90,7 @@ ** 041 OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them. ** CA 20201008 Added API to get an iterator over the property ASTs. The propsMap is no longer ** cleared for normal conversion - just for runtime conversion. +** OM 20210118 Avoid java name conflicts with SQL keywords. */ /* @@ -152,6 +152,7 @@ import java.util.*; import java.util.function.*; import com.goldencode.ast.*; +import com.goldencode.p2j.convert.*; import com.goldencode.util.*; import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.persist.*; @@ -946,15 +947,14 @@ { P2OLookup lookup = getInstance(historical, EntityName.TABLE); List propList = lookup.getProperties(historical); - + if (propList == null || propList.isEmpty()) { throw new SchemaException("Table '" + historical + "' not recognized"); } - Collections.sort(propList, (c1, c2) -> Long.compare((Long) c1.getAnnotation("fieldid"), - (Long) c2.getAnnotation("fieldid"))); - + propList.sort(Comparator.comparingLong(c -> (Long) c.getAnnotation("fieldid"))); + return propList.iterator(); } @@ -1473,23 +1473,20 @@ } /** - * Get the converted, Java name of the class or property associated with - * the given, legacy table or field name (respectively). + * Get the converted, Java name of the class or property associated with the given, legacy table or field + * name (respectively). * * @param historical * Fully qualified, legacy table or field name. Must be normalized (in lowercase). * @param index - * Zero-based index of custom property name for denormalized field with extent, - * otherwise null. + * Zero-based index of custom property name for denormalized field with extent, otherwise null. * - * @return Unqualified, Java class name (for a table) or property name - * (for a field). + * @return Unqualified, Java class name (for a table) or property name (for a field). */ private String getJavaName(String historical, Long index) { - String name = javaNameMap.containsKey(historical) - ? javaNameMap.get(historical).get(index == null ? null : index + 1) - : null; + Map innerMap = javaNameMap.get(historical); + String name = (innerMap != null) ? innerMap.get(index == null ? null : index + 1) : null; if (name != null) { @@ -1503,7 +1500,9 @@ name = parent.getJavaName(historical, index); if (name != null) + { return name; + } } } @@ -1551,7 +1550,9 @@ names = parent.getLegacyFieldNames(historical, includeHidden); if (!names.isEmpty()) + { break; + } } } @@ -1624,7 +1625,9 @@ res = parent.getProperties(historical); if (!res.isEmpty()) + { return res; + } } } @@ -1747,7 +1750,7 @@ } buf.append(nextClass.getAnnotation(HISTORICAL)); String histTable = buf.toString().intern(); - addJavaName(histTable, ifaceName, null, null); + addJavaName(histTable, ifaceName, null, null, NameConverter.TYPE_TABLE); // store the meta tables, if the case if (meta) @@ -1784,18 +1787,14 @@ legacyName = legacyName.toLowerCase().intern(); if (nextProp.isAnnotation("customextent")) { - if (!denormalizedProperties.contains(legacyName)) - { - denormalizedProperties.add(legacyName); - } + denormalizedProperties.add(legacyName); long index = (Long)nextProp.getAnnotation("customextent"); - String denormalizedpropertyname = - (String)nextProp.getAnnotation("denormalizedpropertyname"); - addJavaName(legacyName, nextProp.getText(), index, denormalizedpropertyname); + String dpName= (String) nextProp.getAnnotation("denormalizedpropertyname"); + addJavaName(legacyName, nextProp.getText(), index, dpName, NameConverter.TYPE_COLUMN); } else { - addJavaName(legacyName, nextProp.getText(), null, null); + addJavaName(legacyName, nextProp.getText(), null, null, NameConverter.TYPE_COLUMN); } } @@ -1825,35 +1824,32 @@ } /** - * Adds property name to javaNameMap where the key is legacy field name and value is map - * based on nullable index. Index is not null only for denormalized field with extent, in - * this case index corresponds to denormalization table hint. + * Adds property name to {@code javaNameMap} where the key is legacy field name and value is map based on + * nullable index. Index is not {@code null} only for denormalized field with extent, in this case index + * corresponds to denormalization table hint. * * @param originalName * Legacy field name (not necessarily normalized). * @param propertyName * Property name. * @param index - * Null-based index of custom property name for denormalized field with extent, - * otherwise null. + * Null-based index of custom property name for denormalized field with extent, otherwise null. * @param denormalizedPropertyName - * Denormalized property name for denormalized field with extent, - * otherwise null. + * Denormalized property name for denormalized field with extent, otherwise {@code null}. + * @param type + * One of the {@code NameConverter} TYPE_ constants which helps resolve possible name conflicts. */ private void addJavaName(String originalName, String propertyName, Long index, - String denormalizedPropertyName) + String denormalizedPropertyName, + int type) { String normalized = originalName.toLowerCase().intern(); - propertyName = propertyName.intern(); + propertyName = NameConverter.resolvePossibleKeywordConflict(propertyName, type).intern(); - Map customProperties; - if (javaNameMap.containsKey(normalized)) - { - customProperties = javaNameMap.get(normalized); - } - else + Map customProperties = javaNameMap.get(normalized); + if (customProperties == null) { customProperties = new HashMap<>(); if (index != null && denormalizedPropertyName != null) === modified file 'src/com/goldencode/p2j/schema/RecordLoader.java' --- src/com/goldencode/p2j/schema/RecordLoader.java 2020-09-10 13:08:11 +0000 +++ src/com/goldencode/p2j/schema/RecordLoader.java 2021-01-17 15:39:44 +0000 @@ -32,6 +32,7 @@ ** 007 OM 20200906 New ORM implementation. ** 008 CA 20200910 Fixed overwriteNullProperty - it was resolving the wrong property for extent ** fields. +** 009 IAS 20201224 Added PropertyMapper by column name lookup */ /* @@ -91,6 +92,7 @@ import java.io.*; import java.util.*; + import com.goldencode.p2j.persist.*; import com.goldencode.p2j.persist.orm.*; import com.goldencode.p2j.util.*; @@ -122,6 +124,9 @@ /** Property mappers which define column data, by property name */ private final Map mappersByProp = new HashMap<>(); + /** Property mappers which define column data, by column name */ + private final Map mappersByCol = new HashMap<>(); + /** Property mappers which define column native index, by property name */ private final Map mappersByIndex = new HashMap<>(); @@ -175,11 +180,23 @@ PropertyMapper mapper = new PropertyMapper(meta, meta.getOffset() + index); properties[k] = mapper; mappersByProp.putIfAbsent(meta.getName(), mapper); // keep only the first in series + mappersByCol.putIfAbsent(meta.getColumn(), mapper); // keep only the first in series mappersByIndex.putIfAbsent(meta.getName(), k); } } /** + * Get PropertyMapper by column name. + * @param name + * column name + * @return PropertyMapper for the property with given name. + */ + public PropertyMapper getPropertyMapperByColumn(String name) + { + return mappersByCol.get(name); + } + + /** * Overwrites the {@code null} / {@code unknown} values of a specified property and updates it * to a specified value (if the property is an extent, all indexes are checked and updated). * This happens for mandatory fields because they were declared as not-null. === modified file 'src/com/goldencode/p2j/schema/SchemaDictionary.java' --- src/com/goldencode/p2j/schema/SchemaDictionary.java 2020-10-01 22:14:40 +0000 +++ src/com/goldencode/p2j/schema/SchemaDictionary.java 2020-10-15 07:54:57 +0000 @@ -496,6 +496,8 @@ ** not re-compute the temp-table scope each time the same list of temp-table buffers ** is used. ** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access. +** CA 20201011 Scope.promoted changed to an identity hash set. +** CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation. */ /* @@ -563,6 +565,7 @@ import com.goldencode.p2j.security.*; import com.goldencode.p2j.uast.*; import com.goldencode.util.*; +import com.goldencode.util.Stack; import com.goldencode.p2j.util.*; /** @@ -5779,7 +5782,7 @@ private class Scope { /** Stores the list of name nodes that have been promoted */ - private Set promoted = new HashSet<>(); + private Set promoted = Collections.newSetFromMap(new IdentityHashMap<>()); /** Unique identifier for this scope. */ private int id = -1; === modified file 'src/com/goldencode/p2j/security/SecurityManager.java' --- src/com/goldencode/p2j/security/SecurityManager.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/security/SecurityManager.java 2021-01-13 21:04:41 +0000 @@ -425,6 +425,8 @@ ** a new session when forceSecurityContext() is used. *** CA 20200213 Added setHeadless. ** 130 IAS 20200722 Refactored to the new NetSocket API. +** HC 20201024 Added exception info to an error message. +** 131 IAS 20201214 Additional initialization added to the constructor. */ /* @@ -728,6 +730,16 @@ throw new ConfigurationException("setInitialSecurityContext()", exc); } + try + { + getSecureSocketContext(); + } + catch (UnrecoverableKeyException | KeyManagementException | NoSuchAlgorithmException + | KeyStoreException | ConfigurationException e) + { + throw new ConfigurationException("getSecureSocketContext()", e); + } + init = true; } @@ -3042,7 +3054,7 @@ if (LOG.isLoggable(Level.SEVERE)) { String msg = "Could not load the " + alias + " certificate from the provided keystore"; - LOG.logp(Level.SEVERE, "SecurityManager", "authenticateServer", msg); + LOG.logp(Level.SEVERE, "SecurityManager", "authenticateServer", msg + "\n" + e); } return null; } === modified file 'src/com/goldencode/p2j/uast/ClassDefinition.java' --- src/com/goldencode/p2j/uast/ClassDefinition.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/uast/ClassDefinition.java 2021-01-27 00:57:26 +0000 @@ -2,7 +2,7 @@ ** Module : ClassDefinition.java ** Abstract : defines the 4GL API for a class or interface definition ** -** Copyright (c) 2007-2020, Golden Code Development Corporation. +** Copyright (c) 2007-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ---------------------------Description---------------------------- ** 001 GES 20070911 @35134 First version, with support for resolving member variables, @@ -76,6 +76,18 @@ ** CA 20200529 Fixed an issue for incremental conversion, when the AST for a skeleton class ** (which was parsed in a previous conversion) does not exist. ** 017 CA 20200804 Emit bulk setter and getter for an extent property. +** CA 20210125 Progress.Lang.Class:Invoke and Progress.Reflect.Method:Invoke must be treated as +** having a POLY returned type. +** GES 20210121 Started rework of fuzzy matching to implement missing features. This first +** pass refactors the fuzzyMethodLookup() processing to make a list of all +** possible matches at the top of the method (by using the new candidates() +** method). This approach only returns candidates which match the access rights +** and number of parameters checks. It also returns all candidates from the +** entire inheritance hierarchy. This is how the 4GL does it, so that better +** matches in the inheritance hierarchy take precedence over local matches. +** The parameter mode processing was also switched. INPUT parameter types +** widen from caller to callee and OUTPUT parameter types narrow from caller +** to callee. */ /* @@ -1546,11 +1558,22 @@ if (mdat != null) { - // the type may have been set incorrectly based on guessing, since the parameter - // signature was not yet available; force it to the correct value here - if (node.getType() != FUNC_CLASS) - { - node.setType(mdat.type); + if (mdat.name.equalsIgnoreCase("invoke") && + (mdat.container.name.equalsIgnoreCase("progress.lang.class") || + mdat.container.name.equalsIgnoreCase("progress.reflect.method"))) + { + // set the node's type to OO_METH_POLY + node.putAnnotation("oldtype", (long) node.getType()); + node.setType(OO_METH_POLY); + } + else + { + // the type may have been set incorrectly based on guessing, since the parameter + // signature was not yet available; force it to the correct value here + if (node.getType() != FUNC_CLASS) + { + node.setType(mdat.type); + } } // the access mode won't necessarily be the same as that passed in @@ -3146,7 +3169,7 @@ * * @param name * Resource name. - * @param sig + * @param caller * Method call signature. * @param access * Access mode (KW_PUBLIC, KW_PROTECTD or @@ -3162,15 +3185,16 @@ * @return Resource found or null if no match exists. */ private MemberData fuzzyMethodLookup(String name, - ParameterKey[] sig, + ParameterKey[] caller, int access, boolean isStatic, boolean internal, Aast node) { MemberData mdat = null; + boolean polyParams = false; - for (ParameterKey p : sig) + for (ParameterKey p : caller) { if ("BaseDataType".equals(p.type)) { @@ -3179,290 +3203,441 @@ } } - synchronized (lock) + boolean first = isSetParam(name); + + if ("Progress.Json.ObjectModel.JsonObject".equalsIgnoreCase(this.name) && + "write".equalsIgnoreCase(name) && + caller.length == 2 && + caller[0].type.equals("longchar") && + caller[1].type.equals("logical")) { - Map> map = isStatic ? smethods : imethods; - - if (map != null) - { - Map defs = map.get(name.toLowerCase()); + int bogus = 14; + } + + List list = candidates(name, caller.length, access, isStatic, internal, first, null); - if (defs != null) - { - if ("Progress.Lang.ParameterList".equalsIgnoreCase(this.name) && - "setParameter".equalsIgnoreCase(name)) - { - return defs.values().iterator().next(); - } - - // the fuzzy lookup is done in phases - // 1. collect all matches, regardless of parameter modes - // 2. if only one match found, use that - // 3. if multiple matches found: - // - if one exact match by exact parameter type, use that - // - if multiple matches, get by parameter modes - // > if caller hasn't specified a mode for an argument, use wildcard - // > only one match should be found - // > if multiple matches, show warning - - List matches = new ArrayList<>(); - List exactMatches = new ArrayList<>(); - - Iterator iter = defs.values().iterator(); - outer: - while (iter.hasNext()) - { - MemberData dat = checkAccessRights(iter.next(), access); - - if (dat != null) - { - ParameterKey[] candidate = dat.signature; - - if (sig.length == candidate.length) - { - boolean exact = true; - inner: - for (int i = 0; i < sig.length; i++) - { - String candidateType = candidate[i].type; - String sigType = sig[i].type; - candidateType = fromJava(candidateType); - sigType = fromJava(sigType); - - if (candidateType.equals(sigType)) - { - // exact match, try the next one - continue inner; - } - else - { - if ("TEMP-TABLE".equals(candidateType) || - "TABLE-HANDLE".equals(candidateType)) - { - // handle can be passed to TABLE-HANDLE or DATASET-HANDLE in 4GL - if ("TEMP-TABLE".equals(sigType) || - "TABLE-HANDLE".equals(sigType) || - "handle".equals(sigType)) - { - continue inner; - } - - continue outer; - } - else if ("DATASET".equals(candidateType) || - "DATASET-HANDLE".equals(candidateType)) - { - // handle can be passed to TABLE-HANDLE or DATASET-HANDLE in 4GL, - // FWD doesn't support this at this time, so ignore it here. - if ("DATASET".equals(sigType) || - "DATASET-HANDLE".equals(sigType)) - { - continue inner; - } - - continue outer; - } - else if (candidateType.startsWith("BUFFER")) - { - if (sigType.equals(candidateType)) - { - continue inner; - } - - continue outer; - } - - exact = false; - - // unknown value or BDT (POLY cases) match anything - if (sigType == null || - sigType.equals("BaseDataType") || - sigType.equals("unknown")) - { - continue inner; - } - - // exact match on type but fuzzy extent spec match - int idx1 = candidateType.indexOf("["); - int idx2 = sigType.indexOf("["); - - boolean extentMatch = isExtentMatch(candidateType, sigType); - if (idx1 != -1 && idx2 != -1) - { - candidateType = candidateType.substring(0, idx1); - sigType = sigType.substring(0, idx2); - - int ex1 = getExtent(candidateType); - int ex2 = getExtent(sigType); - extentMatch = extentMatch || ex1 == ex2 || ex1 == -1 || ex2 == -1; - - if (candidateType.equals(sigType)) - { - if (extentMatch) - { - // match - continue inner; - } - else - { - // not a match - continue outer; - } - } - } - - // check the parameter mode at the definition - if (candidate[i].mode == KW_INPUT) - { - // narrowing matches are OK, widening are NOT OK - if (isNarrowingTypeMatch(candidateType, sigType) && extentMatch) - { - // match - continue inner; - } - else - { - // not a match - continue outer; - } - } - else if (candidate[i].mode == KW_OUTPUT) - { - // widening matches are OK, narrowing are NOT OK - if (isWideningTypeMatch(candidateType, sigType) && extentMatch) - { - // match - continue inner; - } - else - { - // not a match - continue outer; - } - } - else if (candidate[i].mode == KW_IN_OUT) - { - // exact match is required - if (!candidateType.equals(sigType)) - { - // not a match - continue outer; - } - } - } - } - - // if we are here, we found a signature match; ignore not the right type - if (dat != null && ((!isStatic && (!dat.isStatic || internal)) || - (isStatic && dat.isStatic))) - { - matches.add(dat); - - if (exact) - { - exactMatches.add(dat); - } - } - else - { - // clear our result - dat = null; - } - } - } - } - - if (matches.size() == 1) - { - mdat = matches.get(0); - return mdat; - } - else if (exactMatches.size() == 1) - { - mdat = exactMatches.get(0); - return mdat; - } - - // multiple fuzzy matches, get by parameter mode - iter = matches.iterator(); - outer: - while (iter.hasNext()) - { - MemberData dat = iter.next(); - ParameterKey[] candidate = dat.signature; - - inner: - for (int i = 0; i < candidate.length; i++) - { - Integer candidateMode = candidate[i].mode; - Integer sigMode = sig[i].mode; - - if (Objects.equals(candidateMode, sigMode) || sigMode == null) - { - // either the same or the caller hasn't specified the argument mode - // TODO: the mode is not mandatory at the definition... assume INPUT? + // quick out if there are no possible matches + if (list.isEmpty()) + { + return null; + } + + // special case for ParameterList.setParameter() + if (first && list.size() == 1) + { + return list.get(0); + } + + // the fuzzy lookup is done in phases + // 1. collect all matches, regardless of parameter modes + // 2. if only one match found, use that + // 3. if multiple matches found: + // - if one exact match by exact parameter type, use that + // - if multiple matches, get by parameter modes + // > if caller hasn't specified a mode for an argument, use wildcard + // > only one match should be found + // > if multiple matches, show warning + + List matches = new ArrayList<>(); + List exactMatches = new ArrayList<>(); + + // TODOS: + // - constructors + // - handle dynamic invocation marking here + // - tables/table handles + // - datasets/dataset handles + // - object fuzziness + // - does primitive type fuzziness have less priority than primitive type exact matches? + + // process each parameter from left to right; the processing will vary by the parameter type; some + // types (e.g. objects) have multi-phase checks and other types are a simple comparison; at each + // step of the way the list of possible candidates will be reduced until there are only candidates + // left which match all criteria; at that point if there are more than one then we either have a + // dynamic invocation scenario or there is some ambiguity (which should not happen if the code + // compiles in the 4GL) + + // OUTLINE FOR NEXT CHANGES + /* + caller: + for (int i = 0; i < caller.length; i++) + { + // if the caller is passing Java types, then we match them as if they were 4GL types so the get + // converted here; if they are already 4GL types then no change will happen + String callerType = fromJava(caller[i].type); + + // wildcards: unknown value or BDT (POLY cases) match anything (no cases can be excluded) + if (callerType == null || + callerType.equals("BaseDataType") || + callerType.equals("unknown")) + { + continue caller; + } + + // primitive types + + // primitive types widening/narrowing + + // tables/table handles + + // handles passed to table + + // datasets/dataset handles + + // handles passed to datasets + + // buffers + + // extents + + // object direct match + + // object fuzziness + + // parameter modes + } + */ + + Iterator iter = list.iterator(); + + outer: + while (iter.hasNext()) + { + MemberData dat = iter.next(); + + ParameterKey[] candidate = dat.signature; + + boolean exact = true; + + inner: + for (int i = 0; i < caller.length; i++) + { + String candidateType = candidate[i].type; + String callerType = caller[i].type; + candidateType = fromJava(candidateType); + callerType = fromJava(callerType); + + if (candidateType.equals(callerType)) + { + // exact match, try the next one + continue inner; + } + else + { + if ("TEMP-TABLE".equals(candidateType) || + "TABLE-HANDLE".equals(candidateType)) + { + // handle can be passed to TABLE-HANDLE or DATASET-HANDLE in 4GL + if ("TEMP-TABLE".equals(callerType) || + "TABLE-HANDLE".equals(callerType) || + "handle".equals(callerType)) + { + continue inner; + } + + continue outer; + } + else if ("DATASET".equals(candidateType) || + "DATASET-HANDLE".equals(candidateType)) + { + // handle can be passed to TABLE-HANDLE or DATASET-HANDLE in 4GL, + // FWD doesn't support this at this time, so ignore it here. + if ("DATASET".equals(callerType) || + "DATASET-HANDLE".equals(callerType)) + { + continue inner; + } + + continue outer; + } + else if (candidateType.startsWith("BUFFER")) + { + if (callerType.equals(candidateType)) + { + continue inner; + } + + continue outer; + } + + exact = false; + + // unknown value or BDT (POLY cases) match anything + if (callerType == null || + callerType.equals("BaseDataType") || + callerType.equals("unknown")) + { + continue inner; + } + + // exact match on type but fuzzy extent spec match + int idx1 = candidateType.indexOf("["); + int idx2 = callerType.indexOf("["); + + boolean extentMatch = isExtentMatch(candidateType, callerType); + + if (idx1 != -1 && idx2 != -1) + { + candidateType = candidateType.substring(0, idx1); + callerType = callerType.substring(0, idx2); + + int ex1 = getExtent(candidateType); + int ex2 = getExtent(callerType); + + extentMatch = extentMatch || ex1 == ex2 || ex1 == -1 || ex2 == -1; + + if (candidateType.equals(callerType)) + { + if (extentMatch) + { + // match continue inner; } else { - // mismatch + // not a match continue outer; } } - - // found a match - if (mdat != null) - { - // add a logging that more than one definition matches this fuzzy - // signature - System.out.println("WARNING: more than one method def found " + - "using fuzzy lookup: " + mdat.name + - " from class " + getName()); - - System.out.printf("\n- access %s, static %b\n" + - " PARAMETERS: %d\n", - ProgressParser.lookupTokenName(mdat.access), - mdat.isStatic, - mdat.signature.length); - for (int i = 0; i < mdat.signature.length; i++) - { - System.out.printf(" %s\n", mdat.signature[i]); - } - - System.out.printf("\n- access %s, static %b\n" + - " PARAMETERS: %d\n", - ProgressParser.lookupTokenName(dat.access), - dat.isStatic, - dat.signature.length); - for (int i = 0; i < dat.signature.length; i++) - { - System.out.printf(" %s\n", dat.signature[i]); - } - - System.out.printf("\nby caller %s\n PARAMETERS: %d\n", - node.dumpTree(true), - sig.length); - for (int i = 0; i < sig.length; i++) - { - System.out.printf(" %s\n", sig[i]); - } - - if (polyParams) - { - return null; - } - - break outer; - } - - mdat = dat; - } - } - } - - // only look up the parent hierarchy if we haven't got a match yet; static lookups only - // work for internal usage, non-static lookups always work - if (mdat == null && parents != null && (!isStatic || internal)) + } + + // check the parameter mode at the definition + if (candidate[i].mode == KW_INPUT) + { + // widening matches are OK, narrowing are NOT OK + if (isWideningTypeMatch(callerType, candidateType) && extentMatch) + { + // match + continue inner; + } + else + { + // not a match + continue outer; + } + } + else if (candidate[i].mode == KW_OUTPUT) + { + // narrowing matches are OK, widening are NOT OK + if (isNarrowingTypeMatch(callerType, candidateType) && extentMatch) + { + // match + continue inner; + } + else + { + // not a match + continue outer; + } + } + else if (candidate[i].mode == KW_IN_OUT) + { + // exact match is required + if (!candidateType.equals(callerType)) + { + // not a match + continue outer; + } + } + } + } + + // if we are here, we found a signature match; ignore not the right type + if (dat != null && ((!isStatic && (!dat.isStatic || internal)) || + (isStatic && dat.isStatic))) + { + matches.add(dat); + + if (exact) + { + exactMatches.add(dat); + } + } + else + { + // clear our result + dat = null; + } + } + + if (matches.size() == 1) + { + mdat = matches.get(0); + return mdat; + } + else if (exactMatches.size() == 1) + { + mdat = exactMatches.get(0); + return mdat; + } + + // multiple fuzzy matches, get by parameter mode + iter = matches.iterator(); + + outer: + while (iter.hasNext()) + { + MemberData dat = iter.next(); + ParameterKey[] candidate = dat.signature; + + inner: + for (int i = 0; i < candidate.length; i++) + { + Integer candidateMode = candidate[i].mode; + Integer callerMode = caller[i].mode; + + if (Objects.equals(candidateMode, callerMode) || callerMode == null) + { + // either the same or the caller hasn't specified the argument mode + // TODO: the mode is not mandatory at the definition... assume INPUT? + continue inner; + } + else + { + // mismatch + continue outer; + } + } + + // found a match + if (mdat != null) + { + // add a logging that more than one definition matches this fuzzy + // signature + System.out.println("WARNING: more than one method def found " + + "using fuzzy lookup: " + mdat.name + + " from class " + getName()); + + System.out.printf("\n- access %s, static %b\n" + + " PARAMETERS: %d\n", + ProgressParser.lookupTokenName(mdat.access), + mdat.isStatic, + mdat.signature.length); + for (int i = 0; i < mdat.signature.length; i++) + { + System.out.printf(" %s\n", mdat.signature[i]); + } + + System.out.printf("\n- access %s, static %b\n" + + " PARAMETERS: %d\n", + ProgressParser.lookupTokenName(dat.access), + dat.isStatic, + dat.signature.length); + for (int i = 0; i < dat.signature.length; i++) + { + System.out.printf(" %s\n", dat.signature[i]); + } + + System.out.printf("\nby caller %s\n PARAMETERS: %d\n", + node.dumpTree(true), + caller.length); + for (int i = 0; i < caller.length; i++) + { + System.out.printf(" %s\n", caller[i]); + } + + if (polyParams) + { + return null; + } + + break outer; + } + + mdat = dat; + } + + return mdat; + } + + /** + * Reports if the class name is Progress.Lang.ParameterList and the method name is {@code setParameter()}. + * + * @param method + * The method name. + * + * @return {@code true} if this is a setParameter call. + */ + private boolean isSetParam(String method) + { + return "Progress.Lang.ParameterList".equalsIgnoreCase(this.name) && + "setParameter".equalsIgnoreCase(method); + } + + /** + * Get the list of all possible methods that could match in this class AND its parent classes. The + * actual types are not checked but the method name, access mode, static/instance and number of + * parameters must all match. + * + * @param name + * Method name. + * @param num + * Number of parameters. + * @param access + * Access mode (KW_PUBLIC, KW_PROTECTD or KW_PRIVATE). + * @param isStatic + * If true, only static methods will be looked up. Otherwise any method can be + * returned. + * @param internal + * {@code true} if the lookup is internal to the current class definition. + * @param first + * {@code true} if the first match should be immediately returned as the result. + * @param exist + * The set of existing methods already represented in the list. + * + * @return List of possible methods that match. This may be an empty list if no match exists. + */ + private List candidates(String name, + int num, + int access, + boolean isStatic, + boolean internal, + boolean first, + Set exist) + { + Set existing = exist == null ? new HashSet<>() : exist; + List results = new ArrayList<>(); + + synchronized (lock) + { + Map> map = isStatic ? smethods : imethods; + + if (map != null) + { + Map defs = map.get(name.toLowerCase()); + + if (defs != null) + { + if (first) + { + results.add(defs.values().iterator().next()); + + return results; + } + + Iterator iter = defs.values().iterator(); + + outer: + while (iter.hasNext()) + { + MemberData dat = checkAccessRights(iter.next(), access); + + if (dat != null && dat.signature.length == num) + { + SignatureKey key = (num > 0) ? new SignatureKey(dat.signature) : null; + + if (num == 0 || !existing.contains(key)) + { + results.add(dat); + existing.add(key); + } + } + } + } + } + + // fuzzy matches are processed up the the parent hierarchy even if there are possible matches in + // the current class; static lookups only work for internal usage, non-static lookups always work + if (parents != null && (!isStatic || internal)) { // change access mode since we aren't allowed to see private stuff in parent int _access = (access == KW_PRIVATE) ? KW_PROTECTD : access; @@ -3470,15 +3645,12 @@ for (ClassDefinition parent : parents) { // recurse up inheritence hierarchy - mdat = parent.fuzzyMethodLookup(name, sig, _access, isStatic, internal, node); - - if (mdat != null) - break; + results.addAll(parent.candidates(name, num, _access, isStatic, internal, first, existing)); } } } - return mdat; + return results; } /** @@ -3536,57 +3708,85 @@ } /** - * Check if the signature type can be widened to the candidate type. - * - * @param cand - * The candidate data type. - * @param sig - * The passed signature data type. + * Check if the callee's type can be written back to a wider caller's type, when viewed from the + * caller to callee, this looks like a "narrowing" operation. + *

+ * Primitive types narrow as follows: + *

+    *  Caller Type       Callee Parameter Types
+    * --------------     ----------------------
+    * int64              integer
+    * decimal            integer, int64
+    * longchar           character
+    * datetime           date
+    * datetime-tz        date, datetime
+    * 
+ *

+ * Object types narrow by these rules: + * + * + * @param caller + * The caller's passed instance's data type. + * @param callee + * The candidate callee method's parameter data type. + * + * @return {@code true} if a narrowing operation is possible. + */ + private boolean isNarrowingTypeMatch(String caller, String callee) + { + if (("int64".startsWith(caller) && "integer".startsWith(callee)) || + ("longchar".startsWith(caller) && "character".startsWith(callee)) || + ("datetime".startsWith(caller) && "date".startsWith(callee)) || + ("datetimetz".startsWith(caller) && ("date".startsWith(callee) || "datetime".startsWith(callee)))) + { + return true; + } + + if (caller.startsWith("object") && callee.startsWith("object")) + { + return true; + } + + return false; + } + + /** + * Check if the caller's type can be matched to a wider type in the callee's signature. + *

+ * Primitive types widen as follows: + *

+    *  Caller Type       Callee Parameter Types
+    * --------------     ----------------------
+    * integer            int64, decimal
+    * int64              decimal
+    * character          longchar
+    * date               datetime, datetime-tz
+    * datetime           datetime-tz
+    * 
+ *

+ * Object types narrow by these rules: + * + * + * @param caller + * The caller's passed instance's data type. + * @param callee + * The candidate callee method's parameter data type. * * @return {@code true} if a widening operation is possible. */ - private boolean isWideningTypeMatch(String cand, String sig) + private boolean isWideningTypeMatch(String caller, String callee) { - if (("int64".startsWith(cand) && "decimal".startsWith(sig)) || - ("integer".startsWith(cand) && ("int64".startsWith(sig) || - "decimal".startsWith(sig))) || - ("character".startsWith(cand) && "longchar".startsWith(sig)) || - ("datetime".startsWith(cand) && "datetimetz".startsWith(sig)) || - ("date".startsWith(cand) && ("datetime".startsWith(sig) || - "datetimetz".startsWith(sig))) || - (cand.startsWith("object") && sig.startsWith("object"))) + if (("int64".startsWith(caller) && "decimal".startsWith(callee)) || + ("integer".startsWith(caller) && ("int64".startsWith(callee) || "decimal".startsWith(callee))) || + ("character".startsWith(caller) && "longchar".startsWith(callee)) || + ("datetime".startsWith(caller) && "datetimetz".startsWith(callee)) || + ("date".startsWith(caller) && ("datetime".startsWith(callee) || "datetimetz".startsWith(callee)))) { - // TODO: the object case needs to confirm that the sig type is a - // parent class return true; } - - return false; - } - - /** - * Check if the signature type can be narrowed to the candidate type. - * - * @param cand - * The candidate data type. - * @param sig - * The passed signature data type. - * - * @return {@code true} if a narrowing operation is possible. - */ - private boolean isNarrowingTypeMatch(String cand, String sig) - { - if (("int64".startsWith(cand) && "integer".startsWith(sig)) || - ("decimal".startsWith(cand) && ("integer".startsWith(sig) || - "int64".startsWith(sig))) || - ("longchar".startsWith(cand) && "character".startsWith(sig)) || - ("datetime".startsWith(cand) && "date".startsWith(sig)) || - ("datetimetz".startsWith(cand) && ("date".startsWith(sig) || - "datetime".startsWith(sig))) || - (cand.startsWith("object") && sig.startsWith("object"))) + + if (caller.startsWith("object") && callee.startsWith("object")) { - // TODO: the object case should check for the sig as exact match or - // as a subclass return true; } === modified file 'src/com/goldencode/p2j/uast/SymbolResolver.java' --- src/com/goldencode/p2j/uast/SymbolResolver.java 2020-09-30 16:32:34 +0000 +++ src/com/goldencode/p2j/uast/SymbolResolver.java 2020-10-24 00:11:03 +0000 @@ -2,7 +2,7 @@ ** Module : SymbolResolver.java ** Abstract : manages the symbol dictionaries and lookup process ** -** Copyright (c) 2004-2020, Golden Code Development Corporation. +** Copyright (c) 2004-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- --------------------------------- Description --------------------------------- ** 001 GES 20041104 @18650 First version, with support for resolving: @@ -436,6 +436,9 @@ ** 093 IAS 20200624 Added checks for null ** 094 CA 20200804 Emit bulk setter and getter for an extent property. ** CA 20200930 Use an exemplar instance to pre-populate the dictionaries at runtime conversion. +** CA 20210113 If a implemented skeleton's Java method name collides with one part of +** NameConverter.isInternalMethodName's, fail immediately, as this means the +** implemented skeleton method's Java name is not following NameConverter's rules. */ /* @@ -494,6 +497,7 @@ package com.goldencode.p2j.uast; import java.io.*; +import java.lang.reflect.*; import java.util.*; import java.util.List; import java.util.Stack; @@ -502,7 +506,6 @@ import org.reflections.*; -import antlr.collections.*; import com.goldencode.ast.*; import com.goldencode.util.*; import antlr.*; @@ -2231,6 +2234,19 @@ clsName = _BaseObject_.class.getName(); } wa.legacyToJava.put(lr.resource().toLowerCase(), clsName); + + // check the method names against NameConverter + for (Method m : cls.getDeclaredMethods()) + { + if (m.isAnnotationPresent(LegacySignature.class) && + NameConverter.isInternalMethodName(m.getName())) + { + String msg = "Skeleton Java method name collides with FWD internal API: " + + cls.getName() + "." + m.toString(); + System.err.println(msg); + System.exit(1); + } + } } } } @@ -6211,7 +6227,7 @@ { String fldName = name; - if (indexFieldSearch && useIndexFieldSearch && name.indexOf(".") == -1) + if (indexFieldSearch && useIndexFieldSearch && name.indexOf('.') == -1) { fldName = String.format("%s.%s", indexTable, name); if (dict.isField(fldName)) === modified file 'src/com/goldencode/p2j/uast/progress.g' --- src/com/goldencode/p2j/uast/progress.g 2020-09-30 16:32:34 +0000 +++ src/com/goldencode/p2j/uast/progress.g 2021-01-15 23:08:08 +0000 @@ -2,7 +2,7 @@ ** Module : progress.g ** Abstract : Progress 4GL Lexer and Parser Grammar ** -** Copyright (c) 2004-2020, Golden Code Development Corporation. +** Copyright (c) 2004-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- ** 001 GES 20041116 @18747 First version implementing a complete lexer @@ -2167,6 +2167,12 @@ ** to re-populate the dictionaries. ** CA 20200930 Use factory.setASTNodeClass(Class) instead of setASTNodeClass(String), to avoid ** loadClass call. +** CA 20201109 Expose the CALENDAR:VALUE as a 'CalendarValue' attribute which follows the 4GL's +** datetime string representation, and not ISO-8601. +** GES 20201125 Added OS-USERID as a FWD-specific 4GL extension. +** EVL 20201216 Added function to interrupt current metafile reporting. +** CA 20210120 During parsing, if an event name is not registered in FWD's keyboards, report it. +** OM 20201120 Added conversion support for KW_DATA_SM function. Fixed get-db-client() type. */ /* @@ -4337,6 +4343,7 @@ KW_MF_GXY; // FWD extension, not real 4GL! KW_MF_GZF; // FWD extension, not real 4GL! KW_MF_INIT; // FWD extension, not real 4GL! + KW_MF_IR; // FWD extension, not real 4GL! KW_MF_MPDF; // FWD extension, not real 4GL! KW_MF_P2MU; // FWD extension, not real 4GL! KW_MF_RP; // FWD extension, not real 4GL! @@ -4533,6 +4540,7 @@ KW_OS_DRV; KW_OS_ERR; KW_OS_G_ENV; + KW_OS_UID; // FWD extension, not real 4GL! KW_OUT_JOIN; KW_OUTER; KW_OUTPUTCT; @@ -5329,7 +5337,7 @@ KW_CALCUFMT; // CALENDAR:CustomFormat KW_CALFMTST; // CALENDAR:FormatStyle KW_CALUPDWN; // CALENDAR:UpDown - KW_CALVALUE; // CALENDAR:DateTimeValue + KW_CALVALUE; // CALENDAR:CalendarValue KW_CLEA_TAB; // begin SIGNATURE KW_CLEA_WIN; KW_GET_SIMG; @@ -7870,7 +7878,7 @@ sym.addBuiltinFunction("get-codepages" , FUNC_CHAR , false); sym.addBuiltinFunction("get-collation" , FUNC_CHAR , false); sym.addBuiltinFunction("get-collations" , FUNC_CHAR , true ); - sym.addBuiltinFunction("get-db-client" , FUNC_LOGICAL , false); + sym.addBuiltinFunction("get-db-client" , FUNC_HANDLE , false); sym.addBuiltinFunction("get-double" , FUNC_DEC , true ); sym.addBuiltinFunction("get-float" , FUNC_DEC , true ); sym.addBuiltinFunction("get-int64" , FUNC_INT64 , false); @@ -8251,7 +8259,7 @@ attrsAndMethods.put( KW_CALTITFG, ATTR_INT ); // FWD CALENDAR:TitleForeColor extension, not real 4GL attrsAndMethods.put( KW_CALTRLFG, ATTR_INT ); // FWD CALENDAR:TrailingForeColor extension, not real 4GL attrsAndMethods.put( KW_CALUPDWN, ATTR_LOGICAL ); // FWD CALENDAR:UpDown extension, not real 4GL - attrsAndMethods.put( KW_CALVALUE, ATTR_CHAR ); // FWD CALENDAR:DateTimeValue extension, not real 4GL + attrsAndMethods.put( KW_CALVALUE, ATTR_CHAR ); // FWD CALENDAR:CalendarValue extension, not real 4GL attrsAndMethods.put( KW_CANC_BRK, METH_LOGICAL ); attrsAndMethods.put( KW_CANCEL_B, ATTR_HANDLE ); attrsAndMethods.put( KW_CANCEL_R, METH_LOGICAL ); @@ -9527,6 +9535,7 @@ sym.addGlobalVariable("opsys" , VAR_CHAR ); sym.addGlobalVariable("os-drives" , VAR_CHAR ); sym.addGlobalVariable("os-error" , VAR_INT ); + sym.addGlobalVariable("os-userid" , VAR_CHAR ); // FWD extension, not real 4GL! sym.addGlobalVariable("page-number" , FUNC_INT ); sym.addGlobalVariable("page-size" , FUNC_INT ); sym.addGlobalVariable("process-architecture", VAR_INT ); @@ -21022,6 +21031,12 @@ if (#s != null) convertStringTo(#s, EVENT); if (#o != null) #o.setType(EVENT); if (#e != null) #e.setType(EVENT); + + // verify if the event is registered in FWD, accross all keyboards + if (!com.goldencode.p2j.ui.Keyboard.registeredEventName(##.getText())) + { + System.err.println("ERROR: event [" + ##.getText() + "] is not registered in FWD!"); + } } ; @@ -29653,6 +29668,7 @@ LA(1) != KW_ROWID && LA(1) != KW_REJECTED && LA(1) != KW_ROW_STAT && + LA(1) != KW_DATA_SM && LA(1) != KW_REC_LEN) || LA(2) == LPARENS }? record_funcs @@ -32590,6 +32606,7 @@ | ro:KW_ROWID^ { saveAndReplaceType(#ro, FUNC_ROWID); } | rj:KW_REJECTED^ { saveAndReplaceType(#rj, FUNC_LOGICAL); } | rs:KW_ROW_STAT^ { saveAndReplaceType(#rs, FUNC_INT); } + | rd:KW_DATA_SM^ { saveAndReplaceType(#rd, FUNC_LOGICAL); } | rl:KW_REC_LEN^ { saveAndReplaceType(#rl, FUNC_INT); } ) lparens nothing = record[true, false, false] rparens @@ -33626,6 +33643,7 @@ new Keyword("calendar" , 0, KW_CALENDAR, false), // FWD CALENDAR extension, not a real 4GL keyword new Keyword("calendarbackcolor" , 0, KW_CALBGCLR, false), // FWD CALENDAR extension new Keyword("calendarforecolor" , 0, KW_CALFGCLR, false), // FWD CALENDAR extension + new Keyword("calendarvalue" , 0, KW_CALVALUE, false), // CALENDAR:CalendarValue extension new Keyword("call" , 0, KW_CALL , true ), new Keyword("call-name" , 0, KW_CALL_NAM, false), new Keyword("call-type" , 0, KW_CALL_TYP, false), @@ -33833,7 +33851,6 @@ new Keyword("dataset-handle" , 0, KW_DSET_HND, true ), new Keyword("date" , 0, KW_DATE , false), new Keyword("datetime" , 0, KW_DATETIME, false), // missing from keyword index and UNTESTED at this time - new Keyword("datetimevalue" , 0, KW_CALVALUE, false), // CALENDAR:VALUE extension new Keyword("datetime-tz" , 0, KW_DATE_TZ , false), // missing from keyword index and UNTESTED at this time new Keyword("date-format" , 6, KW_DATE_FMT, false), new Keyword("date-separator" , 0, KW_DATE_SEP, false), // FWD extension, not a real 4GL keyword @@ -34669,6 +34686,7 @@ new Keyword("mf-get-xy" , 0, KW_MF_GXY , false), // FWD extension, not real 4GL! new Keyword("mf-get-zoom-factor" , 0, KW_MF_GZF , false), // FWD extension, not real 4GL! new Keyword("mf-init" , 0, KW_MF_INIT , false), // FWD extension, not real 4GL! + new Keyword("mf-interrupt-recording" , 0, KW_MF_IR , false), // FWD extension, not real 4GL! new Keyword("mf-make-pdf" , 0, KW_MF_MPDF , false), // FWD extension, not real 4GL! new Keyword("mf-pixels-to-metafile-units" , 0, KW_MF_P2MU , false), // FWD extension, not real 4GL! new Keyword("mf-reset-page" , 0, KW_MF_RP , false), // FWD extension, not real 4GL! @@ -34954,6 +34972,7 @@ new Keyword("os-error" , 0, KW_OS_ERR , false), new Keyword("os-getenv" , 0, KW_OS_G_ENV, false), new Keyword("os-rename" , 0, KW_OS_REN , true ), + new Keyword("os-userid" , 0, KW_OS_UID , false), // FWD-extension, not real 4GL! new Keyword("otherwise" , 0, KW_OTHER , true ), new Keyword("outer-join" , 0, KW_OUT_JOIN, false), // missing in keyword index, found elsewhere in lang ref new Keyword("outer" , 0, KW_OUTER , false), // missing in keyword index, found elsewhere in lang ref === modified file 'src/com/goldencode/p2j/ui/BaseEntity.java' --- src/com/goldencode/p2j/ui/BaseEntity.java 2020-10-04 18:23:10 +0000 +++ src/com/goldencode/p2j/ui/BaseEntity.java 2020-11-23 20:09:32 +0000 @@ -153,6 +153,11 @@ ** SBI 20200414 Moved getCaptionFontSize into WithCaption interface. ** 083 HC 20201004 Performance optimizations of server-client config state ** synchronization. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201112 Avoid pushing the entire frame definition when setting the PARENT attribute. +** CA 20201123 Fixed a regression in CA/20201112, when the widget is re-attached to another +** frame, before being realized. */ /* ** This program is free software: you can redistribute it and/or modify @@ -566,7 +571,7 @@ double col = ConfigHelper.getEffectiveColumn(config); col = col < 0 ? col - 1 : col + 1; - return new decimal(col - getAttr("frameColumnOffset", () -> config.frameColumnOffset)); + return new decimal(col - getAttr("frameColumnOffset", () -> config.frameColumnOffset, true)); } /** @@ -769,7 +774,7 @@ */ public void setFgColor(int fgcolor) { - if (getAttr("fgcolor ", () -> config.fgcolor )== fgcolor) + if (getAttr("fgcolor", () -> config.fgcolor )== fgcolor) { return; } @@ -1120,6 +1125,11 @@ GenericWidget parent = LogicalTerminal.getWidgetForId(getAttr("parentId", () -> config.parentId)); + if (parent == widget) + { + return; + } + if (parent instanceof WidgetContainer) { WidgetContainer cont = (WidgetContainer) parent; @@ -1127,6 +1137,14 @@ { cont.removeWidget(this); } + + // dettach it to the frame + GenericFrame frame = widget.getFrame(); + if (frame != null) + { + this.config.frameId = -1; + frame.deleteDynamicWidget(this, false); + } } this.parent = widget; @@ -1149,10 +1167,13 @@ else { this.config.frameId = -1; + pushScreenDefinition(); } } - - pushScreenDefinition(); + else + { + pushScreenDefinition(); + } } /** @@ -1411,7 +1432,7 @@ double row = ConfigHelper.getEffectiveRow(config); row = row < 0 ? row - 1 : row + 1; - return new decimal(row - getAttr("frameRowOffset", () -> config.frameRowOffset)); + return new decimal(row - getAttr("frameRowOffset", () -> config.frameRowOffset, true)); } /** @@ -1485,7 +1506,7 @@ @Override public void setSelected(boolean sel) { - if (getAttr("selected ", () -> config.selected )== sel) + if (getAttr("selected", () -> config.selected )== sel) { return; } @@ -1549,7 +1570,7 @@ { flushWidgetAttrs(); int x = ConfigHelper.getEffectiveX(config); - return new integer(x - getAttr("frameXOffset", () -> config.frameXOffset)); + return new integer(x - getAttr("frameXOffset", () -> config.frameXOffset, true)); } /** @@ -1586,7 +1607,7 @@ { flushWidgetAttrs(); int y = ConfigHelper.getEffectiveY(config); - return new integer(y - getAttr("frameYOffset", () -> config.frameYOffset)); + return new integer(y - getAttr("frameYOffset", () -> config.frameYOffset, true)); } /** @@ -1644,7 +1665,7 @@ setTooltip((String)null); // deactivate tooltip feature for this widget not depending on SESSION:TOOLTIP value // the client side call to deactivate tooltip for this widget - LogicalTerminal.getClient().deactivateTooltipForWidget(getAttr("id", () -> config.id).asInt()); + LogicalTerminal.getClient().deactivateTooltipForWidget(getId()); } else { @@ -1658,7 +1679,7 @@ if (SessionUtils.getSessionTooltips().booleanValue()) { // the client side call to activate tooltip for this widget - LogicalTerminal.getClient().activateTooltipForWidget(getAttr("id", () -> config.id).asInt()); + LogicalTerminal.getClient().activateTooltipForWidget(getId()); } } } @@ -1838,7 +1859,7 @@ @Override public decimal getFrameColumn() { - return MathOps.plus(getColumn(), getAttr("frameColumnOffset", () -> config.frameColumnOffset)); + return MathOps.plus(getColumn(), getAttr("frameColumnOffset", () -> config.frameColumnOffset, true)); } /** @@ -1849,7 +1870,7 @@ @Override public decimal getFrameRow() { - return MathOps.plus(getRow(), getAttr("frameRowOffset", () -> config.frameRowOffset)); + return MathOps.plus(getRow(), getAttr("frameRowOffset", () -> config.frameRowOffset, true)); } /** @@ -1861,8 +1882,9 @@ public integer getFrameX() { // if we are in ChUI, this actually returns the FRAME-COL - return new integer(LogicalTerminal.isChui() ? getFrameColumn() - : MathOps.plus(getX(), getAttr("frameXOffset", () -> config.frameXOffset))); + return new integer(LogicalTerminal.isChui() + ? getFrameColumn() + : MathOps.plus(getX(), getAttr("frameXOffset", () -> config.frameXOffset, true))); } /** @@ -1874,8 +1896,9 @@ public integer getFrameY() { // if we are in ChUI, this actually returns the FRAME-ROW - return new integer(LogicalTerminal.isChui() ? getFrameRow() - : MathOps.plus(getY(), getAttr("frameYOffset", () -> config.frameYOffset))); + return new integer(LogicalTerminal.isChui() + ? getFrameRow() + : MathOps.plus(getY(), getAttr("frameYOffset", () -> config.frameYOffset, true))); } /** @@ -1912,7 +1935,7 @@ @Override public integer getFontSize() { - return getAttr("fontSize ", () -> config.fontSize )< 0 ? new integer() : new integer(getAttr("fontSize", () -> config.fontSize)); + return getAttr("fontSize", () -> (config.fontSize < 0 ? new integer() : new integer(config.fontSize))); } /** @@ -1939,7 +1962,7 @@ @Override public logical isFontBold() { - return new logical(getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isBold()); + return new logical(getAttr("fontStyle", () -> (config.fontStyle != null && config.fontStyle.isBold()))); } /** @@ -1956,10 +1979,11 @@ return; } - FontStyle newStyleStyle = FontStyle.createStyle(value.booleanValue(), - getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isItalic(), - getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isUnderline()); - setAttr("fontStyle", config.fontStyle, newStyleStyle, (vv) -> config.fontStyle = vv); + FontStyle currFontStyle = getAttr("fontStyle", () -> config.fontStyle); + FontStyle newStyle = FontStyle.createStyle(value.booleanValue(), + currFontStyle != null && currFontStyle.isItalic(), + currFontStyle != null && currFontStyle.isUnderline()); + setAttr("fontStyle", config.fontStyle, newStyle, (vv) -> config.fontStyle = vv); } /** @@ -1970,7 +1994,8 @@ @Override public logical isFontItalic() { - return new logical(getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isItalic()); + return new logical(getAttr("fontStyle", + () -> (config.fontStyle != null && config.fontStyle.isItalic()))); } /** @@ -1987,9 +2012,10 @@ return; } - FontStyle newStyle = FontStyle.createStyle(getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isBold(), - value.booleanValue(), - getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isUnderline()); + FontStyle currFontStyle = getAttr("fontStyle", () -> config.fontStyle); + FontStyle newStyle = FontStyle.createStyle(currFontStyle != null && currFontStyle.isBold(), + value.booleanValue(), + currFontStyle != null && currFontStyle.isUnderline()); setAttr("fontStyle", config.fontStyle, newStyle, (vv) -> config.fontStyle = vv); } @@ -2001,7 +2027,8 @@ @Override public logical isFontUnderline() { - return new logical(getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isUnderline()); + return new logical(getAttr("fontStyle", + () -> (config.fontStyle != null && config.fontStyle.isUnderline()))); } /** @@ -2018,9 +2045,10 @@ return; } - FontStyle newStyle = FontStyle.createStyle(getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isBold(), - getAttr("fontStyle ", () -> config.fontStyle )!= null && getAttr("fontStyle", () -> config.fontStyle).isItalic(), - value.booleanValue()); + FontStyle currFontStyle = getAttr("fontStyle", () -> config.fontStyle); + FontStyle newStyle = FontStyle.createStyle(currFontStyle != null && currFontStyle.isBold(), + currFontStyle != null && currFontStyle.isItalic(), + value.booleanValue()); setAttr("fontStyle", config.fontStyle, newStyle, (vv) -> config.fontStyle = vv); } @@ -2408,7 +2436,7 @@ // config stores 0-based value value = value < 0 ? value + 1 : value - 1; - if (!getAttr("realized ", () -> config.realized )|| isInsideSetup) + if (!getAttr("realized", () -> config.realized, true) || isInsideSetup) { config.initRow = value; config.initY = y; @@ -2421,8 +2449,8 @@ if (!isInsideSetup) { // adjust for frame offset - value += getAttr("frameRowOffset", () -> config.frameRowOffset); - y += getAttr("frameYOffset", () -> config.frameYOffset); + value += getAttr("frameRowOffset", () -> config.frameRowOffset, true); + y += getAttr("frameYOffset", () -> config.frameYOffset, true); // last check for the new position does not place the widget outside the frame // virtual region @@ -2434,7 +2462,8 @@ p = getParent(WindowWidget.class); } - double maxHeight = Coordinate.scale(value + getAttr("heightChars", () -> config.heightChars)); + double maxHeight = Coordinate.scale(value + getAttr("heightChars", + () -> config.heightChars, true)); // TODO: Progress doesn't prevent assignment of the new value when it doesn't fit, it only // shows a warning @@ -2484,7 +2513,7 @@ // config stores 0-based value value = value < 0 ? value + 1 : value - 1; - if (!getAttr("realized ", () -> config.realized )|| isInsideSetup) + if (!getAttr("realized", () -> config.realized, true) || isInsideSetup) { config.initColumn = value; config.initX = x; @@ -2497,8 +2526,8 @@ if (!isInsideSetup) { // adjust for frame offset - value += getAttr("frameColumnOffset", () -> config.frameColumnOffset); - x += getAttr("frameXOffset", () -> config.frameXOffset); + value += getAttr("frameColumnOffset", () -> config.frameColumnOffset, true); + x += getAttr("frameXOffset", () -> config.frameXOffset, true); // last check for the new position does not place the widget outside the frame // virtual region @@ -2510,7 +2539,7 @@ p = getParent(WindowWidget.class); } - double maxWidth = Coordinate.scale(value + getAttr("widthChars", () -> config.widthChars)); + double maxWidth = Coordinate.scale(value + getAttr("widthChars", () -> config.widthChars, true)); // TODO: Progress doesn't prevent assignment of the new value when it doesn't fit, it only // shows a warning @@ -2599,7 +2628,7 @@ if (!isInsideSetup) { // adjust for frame offset first - value += getAttr("frameXOffset", () -> config.frameXOffset); + value += getAttr("frameXOffset", () -> config.frameXOffset, true); } double chValue = cc.columnFromPixels(value); @@ -2608,7 +2637,7 @@ chValue = Coordinate.adjustMinMaxGuiChar(chValue); } - if (!getAttr("realized ", () -> config.realized )|| isInsideSetup) + if (!getAttr("realized", () -> config.realized, true) || isInsideSetup) { config.initX = value; config.initColumn = chValue; @@ -2630,7 +2659,7 @@ if (!isInsideSetup) { // adjust for frame offset first - value += getAttr("frameYOffset", () -> config.frameYOffset); + value += getAttr("frameYOffset", () -> config.frameYOffset, true); } double chValue = cc.rowFromPixels(value); @@ -2639,7 +2668,7 @@ chValue = Coordinate.adjustMinMaxGuiChar(chValue); } - if (!getAttr("realized ", () -> config.realized )|| isInsideSetup) + if (!getAttr("realized", () -> config.realized, true) || isInsideSetup) { config.initY = value; config.initRow = chValue; @@ -2767,7 +2796,7 @@ // TO-DO: // value > screen-width - frame-width should produce PROGRESS error 4054 // "**Unable to set COL. FRAME f does not fit in WINDOW widget. (4054)" - if (!validColumnOrRow(isUnknown, value, row) && getAttr("realized", () -> config.realized)) + if (!validColumnOrRow(isUnknown, value, row) && getAttr("realized", () -> config.realized, true)) { // if the widget was already realized and this is not a valid coordinate, do nothing return; @@ -2778,7 +2807,7 @@ value = isChui ? Coordinate.scale(value, 0) : Coordinate.scale(value); value = isChui ? Coordinate.adjustMinMaxChuiChar(value) : Coordinate.adjustMinMaxGuiChar(value); - if ((row && getAttr("row ", () -> config.row )== value - 1) || (!row && getAttr("column ", () -> config.column )== value - 1)) + if ((row && getAttr("row", () -> config.row )== value - 1) || (!row && getAttr("column", () -> config.column )== value - 1)) { return; } @@ -2825,7 +2854,7 @@ return; } - if ((x && getAttr("x ", () -> config.x )== value - 1) || (!x && getAttr("y ", () -> config.y )== value - 1)) + if ((x && getAttr("x", () -> config.x )== value - 1) || (!x && getAttr("y", () -> config.y )== value - 1)) { return; } @@ -3146,8 +3175,9 @@ */ protected decimal _getWidthChars() { - return new decimal(getAttr("clientWidthChars ", () -> config.clientWidthChars )!= BaseConfig.INV_COORD ? - getAttr("clientWidthChars ", () -> config.clientWidthChars ): getAttr("widthChars", () -> config.widthChars)); + double clientWidthCh = getAttr("clientWidthChars", () -> config.clientWidthChars, true); + return new decimal(clientWidthCh != BaseConfig.INV_COORD ? clientWidthCh : + getAttr("widthChars", () -> config.widthChars, true)); } /** @@ -3157,8 +3187,9 @@ */ protected decimal _getHeightChars() { - return new decimal(getAttr("clientHeightChars ", () -> config.clientHeightChars )!= BaseConfig.INV_COORD ? - getAttr("clientHeightChars ", () -> config.clientHeightChars ): getAttr("heightChars", () -> config.heightChars)); + double clientHeightCh = getAttr("clientHeightChars", () -> config.clientHeightChars, true); + return new decimal(clientHeightCh != BaseConfig.INV_COORD ? clientHeightCh : + getAttr("heightChars", () -> config.heightChars, true)); } /** @@ -3169,8 +3200,9 @@ */ protected integer _getWidthPixels() { - return new integer(getAttr("clientWidthPixels ", () -> config.clientWidthPixels )!= BaseConfig.INV_COORD ? - getAttr("clientWidthPixels ", () -> config.clientWidthPixels ): getAttr("widthPixels", () -> config.widthPixels)); + int clientWidthPix = getAttr("clientWidthPixels", () -> config.clientWidthPixels, true); + return new integer(clientWidthPix != BaseConfig.INV_COORD ? clientWidthPix : + getAttr("widthPixels", () -> config.widthPixels, true)); } /** @@ -3180,8 +3212,9 @@ */ protected integer _getHeightPixels() { - return new integer(getAttr("clientHeightPixels ", () -> config.clientHeightPixels )!= BaseConfig.INV_COORD ? - getAttr("clientHeightPixels ", () -> config.clientHeightPixels ): getAttr("heightPixels", () -> config.heightPixels)); + int clientHeightPix = getAttr("clientHeightPixels", () -> config.clientHeightPixels, true); + return new integer(clientHeightPix != BaseConfig.INV_COORD ? clientHeightPix : + getAttr("heightPixels", () -> config.heightPixels, true)); } /** @@ -3191,8 +3224,8 @@ */ private boolean isRedirected() { - return UnnamedStreams.isOut() || - (getAttr("frameId ", () -> config.frameId )!= -1 && LogicalTerminal.getFrame(getAttr("frameId", () -> config.frameId)).isOutRedirected()); + int frId = getAttr("frameId", () -> config.frameId); + return UnnamedStreams.isOut() || (frId != -1 && LogicalTerminal.getFrame(frId).isOutRedirected()); } /** === modified file 'src/com/goldencode/p2j/ui/BrowseColumnWidget.java' --- src/com/goldencode/p2j/ui/BrowseColumnWidget.java 2020-09-28 10:09:11 +0000 +++ src/com/goldencode/p2j/ui/BrowseColumnWidget.java 2020-10-31 00:42:17 +0000 @@ -2,9 +2,9 @@ ** Module : BrowseColumnWidget.java ** Abstract : server side browse column widget implementation ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23480 Created initial version. ** 002 SIY 20060227 @25122 Changes for the browse controller ** implementation. @@ -112,6 +112,14 @@ ** 068 AIL 20200827 Properly displaying error 382 when setting screen value. ** SVL 20200913 Added updateElementAccessor. ** SVL 20200925 Added getExpressionFieldReferences and updateExpressionFieldReferences. +** EVL 20201022 Optimized attributes flush implementation. +** SVL 20201110 name() should return field name, not hibernate property name. +** SVL 20201119 Support for column:FONT attribute in a ROW-DISPLAY trigger. +** SVL 20201216 Fixed getParent(). +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. +** OM 20201030 Invalid attribute API support for getters/setters. */ /* @@ -238,6 +246,11 @@ private boolean comboBoxListMode = true; /** + * The parent browse. + */ + private BrowseWidget browse; + + /** * Default constructor creates a static browse column */ public BrowseColumnWidget() @@ -297,6 +310,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) @Override public void setReadOnly(logical r) { @@ -327,7 +341,7 @@ @Override public integer getInnerLines() { - return new integer(getAttr("innerLines", () -> config.innerLines)); + return new integer(getAttr("innerLines", () -> config.innerLines, true)); } /** @@ -588,7 +602,7 @@ @Override public decimal getWidthChars() { - return new decimal(getAttr("widthChars", () -> config.widthChars)); + return new decimal(getAttr("widthChars", () -> config.widthChars, true)); } /** @@ -599,7 +613,7 @@ @Override public integer getWidthPixels() { - return new integer(getAttr("widthPixels", () -> config.widthPixels)); + return new integer(getAttr("widthPixels", () -> config.widthPixels, true)); } /** @@ -652,6 +666,18 @@ } /** + * Gets the PARENT writable attribute. Returns the parent browse. + * + * @return the parent widget for this widget + */ + @Override + public CommonWidget getParent() + { + // SVL: I couldn't find a way to set parent for a column in 4GL. So always return the parent browse. + return getBrowse(); + } + + /** * Set AUTO-COMPLETION attribute value. * * @param autoCompletion @@ -1102,6 +1128,30 @@ } /** + * Set the FONT attribute of this widget. + * + * @param fontNum + * An entry in the font-table or unknown to refer the default font. + */ + @Override + public void setFont(int64 fontNum) + { + BrowseWidget browse = getBrowse(); + if (browse.isInRowDisplayTrigger()) + { + if (!fontNum.isUnknown() && !LogicalTerminal.isChui()) + { + GuiCellAttributes attr = (GuiCellAttributes) browse.getRowInDisplayAttributes(config().ordinal); + attr.font = fontNum.intValue(); + } + } + else + { + super.setFont(fontNum); + } + } + + /** * Sets the FGCOLOR writable attribute. * * @param fgColor @@ -1119,7 +1169,7 @@ * The new value for the SCREEN-VALUE attribute. */ @Override - public void setScreenValue(character screenValue) + protected void setScreenValueInt(character screenValue) { BrowseWidget browse = getBrowse(); @@ -1144,7 +1194,7 @@ try { setModified(new logical(!getScreenValue().equals(screenValue))); - super.setScreenValue(screenValue); + super.setScreenValueInt(screenValue); } catch (ErrorConditionException e) { @@ -1166,17 +1216,6 @@ } /** - * Sets the SCREEN-VALUE writable attribute. - * - * @param screenValue - * The new value for the SCREEN-VALUE attribute. - */ - public void setScreenValue(String screenValue) - { - setScreenValue(new character(screenValue)); - } - - /** * Gets the SCREEN-VALUE. * * @return The screen value. @@ -1699,7 +1738,9 @@ Accessor acc = ((Element) element).getDataAccessor(); if (acc instanceof FieldReference) { - result = new character(((FieldReference) acc).getProperty()); + FieldReference ref = (FieldReference) acc; + Buffer buf = (Buffer) ref.getParentBuffer().getDMOProxy(); + result = new character(TableMapper.getLegacyFieldName(buf, ref.getProperty())); } } @@ -2314,7 +2355,12 @@ */ public BrowseWidget getBrowse() { - return (BrowseWidget) LogicalTerminal.getWidgetForId(config().browseId); + if (browse == null) + { + browse = (BrowseWidget) LogicalTerminal.getWidgetForId(config().browseId); + } + + return browse; } /** @@ -2584,24 +2630,19 @@ return super.validateFields(enabledOnly); } - + /** * Raise error 4052 "<ATTRIBUTE-NAME> is not a setable attribute for <WIDGET-TYPE> * <COLUMN-NAME>". * - * @param attributeName - * Attribute name. + * @param attributeName + * Attribute name. */ private void errorEditorAttributeNotSettable(String attributeName) { - ErrorManager.recordOrShowError(4052, - String.format("**%s is not a setable attribute for %s %s", - attributeName, - config.widgetType, - name().toStringMessage()), - false, false); + handle.invalidAttribute(attributeName, "setable", config.widgetType.toString(), name().toStringMessage()); } - + /** * Checks if the column is a combo-box column. If not, raises "<ATTRIBUTE> is * not a setable attribute" error. === modified file 'src/com/goldencode/p2j/ui/BrowseInterface.java' --- src/com/goldencode/p2j/ui/BrowseInterface.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/BrowseInterface.java 2020-12-25 16:37:53 +0000 @@ -34,6 +34,7 @@ ** 023 CA 20190703 Added addCalcColumn(character, character, character, String) version. ** 024 CA 20200331 Added setTitleColor and setTitleDColor. ** 025 GES 20200629 Added an addCalcColumn() variant. +** SVL 20201225 MIN-HEIGHT-CHARS moved to MinHeightCharsInterface. */ /* ** This program is free software: you can redistribute it and/or modify @@ -1605,32 +1606,6 @@ public void setMaxHeightChars(decimal max); /** - * Implements the MIN-HEIGHT-CHARS attribute getter.

- * - * @return minimum height of the browse, in character units. - */ - @LegacyAttribute(name = "MIN-HEIGHT-CHARS") - public decimal getMinHeightChars(); - - /** - * Sets the MIN-HEIGHT-CHARS writable attribute. - * - * @param min - * The new value for the MIN-HEIGHT-CHARS attribute. - */ - @LegacyAttribute(name = "MIN-HEIGHT-CHARS", setter = true) - public void setMinHeightChars(NumberType min); - - /** - * Sets the MIN-HEIGHT-CHARS writable attribute. - * - * @param min - * The new value for the MIN-HEIGHT-CHARS attribute. - */ - @LegacyAttribute(name = "MIN-HEIGHT-CHARS", setter = true) - public void setMinHeightChars(decimal min); - - /** * Implements the ALLOW-COLUMN-SEARCHING attribute getter.

* * @return true if column searching is allowed for the browse. === modified file 'src/com/goldencode/p2j/ui/BrowseWidget.java' --- src/com/goldencode/p2j/ui/BrowseWidget.java 2020-10-07 11:19:20 +0000 +++ src/com/goldencode/p2j/ui/BrowseWidget.java 2021-01-23 18:01:03 +0000 @@ -2,7 +2,7 @@ ** Module : BrowseWidget.java ** Abstract : server side BROWSE widget class ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- ** 001 NVS 20051026 @23481 Created initial version. @@ -286,6 +286,20 @@ ** expression. ** SVL 20200927 Fixed NPE for field references not participating in a "compatible" query ** assigned. +** HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. +** SVL 20201028 Fixed finishSetup not called if a dynamic browse becomes visible when the parent +** frame is already visible. +** SVL 20201111 Support for empty LABEL / COLUMN-LABEL. +** SVL 20201221 The widget now implements MinHeightCharsInterface. +** VVT 20210104 getMinHeightChars(): cast added to resolve the ambiguity. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** VVT 20210114 getMaxHeightChars(),setMaxHeightChars(): MAX-HEIGHT-CHARS attribute name must be +** used instead of MAX-HEIGHT +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. Some unnecessary code removed. +** CA 20210123 Setting the COMBO-BOX column's data-type clears its items (as it's dynamic) - this +** was solved by setting the DATA-TYPE before the ID is set. */ /* @@ -381,7 +395,8 @@ RowDisplayDrawListener, WriteProtectable, TitledElement, - QueryOpenedListener + QueryOpenedListener, + MinHeightCharsInterface { /** Logger */ private static final Logger LOG = LogHelper.getLogger(BrowseWidget.class.getName()); @@ -564,6 +579,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) @Override public void setReadOnly(logical r) { @@ -600,7 +616,7 @@ */ public logical isModified() { - return new logical(getAttr("modified", () -> config.modified)); + return new logical(getAttr("modified", () -> config.modified, true)); } /** @@ -660,7 +676,7 @@ */ public void setMultiple(boolean multiple) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { errorAlreadyRealized("MULTIPLE"); return; @@ -771,7 +787,7 @@ */ public void setRowMarkers(boolean markers) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { errorAlreadyRealized("ROW-MARKERS"); return; @@ -891,7 +907,7 @@ */ public void setLabels(boolean labels) { - if (getAttr("realized ", () -> config.realized )&& LogicalTerminal.isChui()) + if (getAttr("realized", () -> config.realized, true) && LogicalTerminal.isChui()) { errorAlreadyRealized("LABELS"); return; @@ -1215,7 +1231,7 @@ return; } - if (getAttr("noValidate ", () -> config.noValidate )== value) + if (getAttr("noValidate", () -> config.noValidate )== value) { return; } @@ -1575,7 +1591,7 @@ if (createOnAdd.isUnknown()) return; - if (getAttr("createOnAdd ", () -> config.createOnAdd )== createOnAdd.booleanValue()) + if (getAttr("createOnAdd", () -> config.createOnAdd )== createOnAdd.booleanValue()) { return; } @@ -2023,7 +2039,7 @@ */ public void setTitle(String title) { - if (getAttr("realized", () -> config.realized) && "".equals(config.title)) + if (getAttr("realized", () -> config.realized, true) && "".equals(config.title)) { // We are free to change TITLE of a realized browse with a title. But we cannot set a // TITLE for a realized browse without title. @@ -2116,7 +2132,7 @@ @Override public integer visibleIterations() { - return getAttr("realized", () -> config.realized) ? getDown() : new integer(); + return getAttr("realized", () -> config.realized, true) ? getDown() : new integer(); } /** @@ -3140,13 +3156,10 @@ String columnLabel; int extentIndex = fieldReference.getIndex(); - // TODO currently P2J returns an empty string if a column label is not specified, - // so it is not clear how distinguish cases when column label is not specified and - // when an empty one is specified columnLabel = TableMapper.getLegacyFieldColumnLabel(buffer, property); - if (columnLabel == null || columnLabel.isEmpty()) // TODO fix + if (columnLabel == null) columnLabel = TableMapper.getLegacyFieldLabel(buffer, property); - if (columnLabel == null || columnLabel.isEmpty()) // TODO fix + if (columnLabel == null) columnLabel = TableMapper.getLegacyFieldName(buffer, property); if (extentIndex != -1) @@ -3533,10 +3546,11 @@ * * @return maximum height of the browse, in character units. */ + @LegacyAttribute(name = "MAX-HEIGHT-CHARS", ignore = true) @Override public decimal getMaxHeightChars() { - notQueryable("MAX-HEIGHT"); + notQueryable("MAX-HEIGHT-CHARS"); return new decimal(); } @@ -3561,7 +3575,7 @@ @Override public void setMaxHeightChars(decimal max) { - ErrorManager.displayError(4052, "MAX-HEIGHT is not a setable attribute for BROWSE "+ + ErrorManager.displayError(4052, "MAX-HEIGHT-CHARS is not a setable attribute for BROWSE "+ widgetName()); } @@ -3585,20 +3599,7 @@ @Override public void setMinHeightChars(NumberType min) { - setMinHeightChars(new decimal(min)); - } - - /** - * Sets the MIN-HEIGHT-CHARS writable attribute. - * - * @param min - * The new value for the MIN-HEIGHT-CHARS attribute. - */ - @Override - public void setMinHeightChars(decimal min) - { - ErrorManager.displayError(4052, "MIN-HEIGHT is not a setable attribute for BROWSE "+ - widgetName()); + ErrorManager.displayError(4052, "MIN-HEIGHT is not a setable attribute for BROWSE " + widgetName()); } /** @@ -3684,7 +3685,7 @@ if (maxDataGuess.isUnknown()) return; - if (getAttr("maxDataGuess ", () -> config.maxDataGuess )== maxDataGuess.intValue()) + if (getAttr("maxDataGuess", () -> config.maxDataGuess )== maxDataGuess.intValue()) { return; } @@ -3933,8 +3934,6 @@ { notSettable("TITLE-BGCOLOR"); // Read-only } - - return; } /** @@ -3952,7 +3951,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized, always ? (unknown) + if (!getAttr("realized", () -> config.realized, true)) // Unrealized, always ? (unknown) { return new integer(); } @@ -3999,8 +3998,6 @@ setAttr("titleDColor", config.titleDColor, new Color(0, (int) dcolor), val -> config.titleDColor = val); } - - return; } /** @@ -4054,8 +4051,6 @@ { notSettable("TITLE-FGCOLOR"); // Read-only } - - return; } /** @@ -4072,7 +4067,7 @@ } else // ChUI, 0 before being realized, ? (unknown) after { - return (!getAttr("realized", () -> config.realized)) ? new integer(0) : new integer(); + return (!getAttr("realized", () -> config.realized, true)) ? new integer(0) : new integer(); } } @@ -4103,8 +4098,6 @@ { notSettable("TITLE-FONT"); // Read-only } - - return; } /** @@ -4592,7 +4585,7 @@ if (searchMode) { // trim to DOWN size - if (data.size() > getAttr("down", () -> config.down)) + if (data.size() > getAttr("down", () -> config.down, true)) { data.remove(0); } @@ -4684,7 +4677,7 @@ // lower part (optional) = = // = = - int rowsToFetch = getAttr("down", () -> config.down) - result.length; + int rowsToFetch = getAttr("down", () -> config.down, true) - result.length; if (rowsToFetch > 0) { int divisionRow = result[0].rowIndex; @@ -4783,15 +4776,17 @@ if (currentRowId.isUnknown()) { // empty result set or some current row sync error - return getRows(0, getAttr("down", () -> config.down), true, rowDisplayTriggerId, sb, displayStrategy); + return getRows(0, getAttr("down", () -> config.down, true), true, rowDisplayTriggerId, sb, + displayStrategy); } int currentRow = currentRowId.intValue() - 1; if (viewportTopRow >= 0 && viewportTopRow >= currentRow) { // current row is before the view - page up (by DOWN - 1) - return getRows(Math.max(0, viewportTopRow - (getAttr("down", () -> config.down ) - 1)), - getAttr("down", () -> config.down), + int downValue = getAttr("down", () -> config.down, true); + return getRows(Math.max(0, viewportTopRow - (downValue - 1)), + downValue, true, rowDisplayTriggerId, sb, @@ -4806,7 +4801,7 @@ { // current row is after the view - restore the same view return getRows(viewportTopRow, - getAttr("down", () -> config.down), + getAttr("down", () -> config.down, true), true, rowDisplayTriggerId, sb, @@ -5129,7 +5124,7 @@ */ public integer getDown() { - return new integer(getAttr("down", () -> config.down)); + return new integer(getAttr("down", () -> config.down, true)); } /** @@ -5190,6 +5185,7 @@ * * @return Always returns the unknown value. */ + @LegacyAttribute(name = "SCREEN-VALUE", ignore = true) @Override public character getScreenValue() { @@ -5203,8 +5199,9 @@ * @param value * Ignored. */ + @LegacyAttribute(name = "SCREEN-VALUE", setter = true, ignore = true) @Override - public void setScreenValue(character value) + protected void setScreenValueInt(character value) { notSettable("SCREEN-VALUE"); } @@ -5272,7 +5269,7 @@ */ public integer getSeparatorFgColor() { - return getAttr("separatorFgColor ", () -> config.separatorFgColor) == -1 ? new integer() : + return getAttr("separatorFgColor", () -> config.separatorFgColor) == -1 ? new integer() : new integer(getAttr("separatorFgColor", () -> config.separatorFgColor)); } @@ -5410,18 +5407,35 @@ } /** + * Set the VISIBLE writable attribute. + * + * @param visible + * The new value for the VISIBLE attribute. + */ + @Override + public void setVisible(boolean visible) + { + if (visible && frame != null && frame._isVisible()) + { + finishSetup(); + } + + super.setVisible(true); + } + + /** * Finish initialization of the browse widget. */ public void finishSetup() { + if (initDone) + return; + if (getAttr("applyEnhancedLayout", () -> config.applyEnhancedLayout) && !enhancedInitDone && query != null) { enhancedInit(); } - if (initDone) - return; - if (query == null) { initDone = true; @@ -7182,7 +7196,7 @@ { return this.getClass().getName() + " id=" + Utils.nonNull(getId(), "") + - " proc=" + getAttr("ehKeyProcName ", () -> config.ehKeyProcName )+ + " proc=" + getAttr("ehKeyProcName", () -> config.ehKeyProcName )+ " name=" + getAttr("ehKeyBrowseName", () -> config.ehKeyBrowseName); } @@ -7374,8 +7388,10 @@ }; } + // WARNING: this needs to be done before the ID is set, otherwise the combo-box items are lost! + editor.setDataType(columnConfig.dataType); + editor.setId(WidgetId.nextID().asInt()); - editor.setDataType(columnConfig.dataType); ValidationExpr validationExpr = column.getValidationExpression(); editor.setValidation(validationExpr, column.getValidationMessage()); column.config.validatable = validationExpr != null; @@ -7666,7 +7682,7 @@ config.format = "x(" + len + ")"; // reset displayFormat to reflect the format change - if (getAttr("displayFormat ", () -> config.displayFormat )!= null) + if (getAttr("displayFormat", () -> config.displayFormat )!= null) { config.displayFormat = null; Browse.getDisplayFormat(config); @@ -7698,7 +7714,7 @@ return false; } - if (getAttr("realized", () -> config.realized) && LogicalTerminal.isChui()) + if (getAttr("realized", () -> config.realized, true) && LogicalTerminal.isChui()) { errorAlreadyRealized("WIDTH"); return false; @@ -7725,7 +7741,7 @@ return false; } - if (getAttr("realized", () -> config.realized) && LogicalTerminal.isChui()) + if (getAttr("realized", () -> config.realized, true) && LogicalTerminal.isChui()) { errorAlreadyRealized("HEIGHT"); return false; @@ -8043,7 +8059,7 @@ ScreenBuffer[] sb, RowDisplayStrategy displayStrategy) { - int rowsToFetch = getAttr("down", () -> config.down) - upper.length; + int rowsToFetch = getAttr("down", () -> config.down, true) - upper.length; if (rowsToFetch > 0) { // still not enough rows to fill the view @@ -8096,7 +8112,7 @@ */ private boolean isRowOnFirstPage(int targetRow) { - return targetRow < getAttr("down", () -> config.down); // TODO take row gaps into account? + return targetRow < getAttr("down", () -> config.down, true); // TODO take row gaps into account? } /** === modified file 'src/com/goldencode/p2j/ui/ButtonListWidget.java' --- src/com/goldencode/p2j/ui/ButtonListWidget.java 2020-09-27 22:30:12 +0000 +++ src/com/goldencode/p2j/ui/ButtonListWidget.java 2020-10-21 23:01:05 +0000 @@ -13,6 +13,7 @@ ** SBI 20200306 Implemented addGroup, getCurrentGroup and setCurrentGroup. ** 003 SBI 20200409 Added updateListOfGroups(), implemented setItemFont, setButtonFont. ** 20200413 Implemented WithCaption interface. +** 004 EVL 20201021 More optimization for getAttr() usage or widget ID getting. */ /* @@ -127,14 +128,18 @@ @Override public handle getCurrentGroup() { - if (groupsList.isEmpty() || - getAttr("currentGroupIndex", () -> config.currentGroupIndex) < 0 || - getAttr("currentGroupIndex", () -> config.currentGroupIndex) >= groupsList.size()) - { - return new handle(); - } - - ButtonListGroupWidget groupWidget = groupsList.get(getAttr("currentGroupIndex", () -> config.currentGroupIndex)); + if (groupsList.isEmpty()) + { + return new handle(); + } + + int currGroupNdx = getAttr("currentGroupIndex", () -> config.currentGroupIndex); + if (currGroupNdx < 0 || currGroupNdx >= groupsList.size()) + { + return new handle(); + } + + ButtonListGroupWidget groupWidget = groupsList.get(currGroupNdx); return groupWidget.asWidgetHandle(); } @@ -325,8 +330,8 @@ @Override public integer getCaptionFontSize() { - return getAttr("captionFontSize", () -> config.captionFontSize) < 0 ? new integer() : - new integer(getAttr("captionFontSize", () -> config.captionFontSize)); + return getAttr("captionFontSize", + () -> (config.captionFontSize < 0 ? new integer() : new integer(config.captionFontSize))); } /** === modified file 'src/com/goldencode/p2j/ui/ButtonWidget.java' --- src/com/goldencode/p2j/ui/ButtonWidget.java 2020-09-27 20:52:31 +0000 +++ src/com/goldencode/p2j/ui/ButtonWidget.java 2021-01-15 20:55:46 +0000 @@ -65,6 +65,7 @@ ** 038 CA 20180313 Added getters for image related attributes. ** 039 SVL 20190317 Button with NO-FOCUS option is not a tab widget. ** 040 VVT 20200203 Extra items removed from the class 'implements' list. +** 041 VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. */ /* ** This program is free software: you can redistribute it and/or modify @@ -1378,6 +1379,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true) @Override public void setConvert3D(boolean flag) { @@ -1390,6 +1392,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true) @Override public void setConvert3D(logical flag) { @@ -1404,6 +1407,7 @@ * * @return CONVERT-3D-COLORS current value. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS") @Override public logical getConvert3D() { @@ -1431,6 +1435,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name) { @@ -1449,6 +1454,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset) { @@ -1469,6 +1475,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset) { @@ -1491,6 +1498,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset, long width) { @@ -1519,6 +1527,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset, long width, long height) { @@ -1537,6 +1546,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name) { @@ -1556,6 +1566,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset) { @@ -1577,6 +1588,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, int64 yOffset) { @@ -1600,6 +1612,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, int64 yOffset, int64 width) { @@ -1625,6 +1638,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, === modified file 'src/com/goldencode/p2j/ui/Calendar.java' --- src/com/goldencode/p2j/ui/Calendar.java 2020-02-25 13:01:04 +0000 +++ src/com/goldencode/p2j/ui/Calendar.java 2020-11-09 21:54:37 +0000 @@ -4,8 +4,10 @@ ** ** Copyright (c) 2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description----------------------------------- +** -#- -I- --Date-- --------------------------------------Description----------------------------------------- ** 001 SBI 20200114 Created initial version. +** 002 CA 20201109 Expose the CALENDAR:VALUE as a 'CalendarValue' attribute which follows the 4GL's datetime +** string representation, and not ISO-8601. */ /* @@ -85,8 +87,8 @@ * * @return The current date time value as a character value */ - @LegacyAttribute(name = "DateTimeValue") - public character getDateTimeValue(); + @LegacyAttribute(name = "CalendarValue") + public character getCalendarValue(); /** * Sets the current date value. @@ -94,8 +96,8 @@ * @param value * The new current date value */ - @LegacyAttribute(name = "DateTimeValue", setter = true) - public void setDateTimeValue(date value); + @LegacyAttribute(name = "CalendarValue", setter = true) + public void setCalendarValue(date value); /** * Sets the current date and time value. @@ -103,8 +105,8 @@ * @param value * The new current date and time value */ - @LegacyAttribute(name = "DateTimeValue", setter = true) - public void setDateTimeValue(datetime value); + @LegacyAttribute(name = "CalendarValue", setter = true) + public void setCalendarValue(datetime value); /** * Sets the current date and time value as a character value. @@ -112,8 +114,8 @@ * @param value * The new current date and time value as a character value */ - @LegacyAttribute(name = "DateTimeValue", setter = true) - public void setDateTimeValue(character value); + @LegacyAttribute(name = "CalendarValue", setter = true) + public void setCalendarValue(character value); /** * Sets the current date and time value representing by a string. @@ -121,8 +123,8 @@ * @param value * The new current date and time value representing by a string */ - @LegacyAttribute(name = "DateTimeValue", setter = true) - public void setDateTimeValue(String value); + @LegacyAttribute(name = "CalendarValue", setter = true) + public void setCalendarValue(String value); /** * Determines whether dates and times are displayed using standard or custom formatting. === modified file 'src/com/goldencode/p2j/ui/CalendarConfig.java' --- src/com/goldencode/p2j/ui/CalendarConfig.java 2020-09-29 02:32:29 +0000 +++ src/com/goldencode/p2j/ui/CalendarConfig.java 2020-12-09 18:05:56 +0000 @@ -9,6 +9,7 @@ ** 002 SBI 20200329 Added the initial values for rgb values. ** 003 IAS 20200823 Rework (de)serialization. ** EVL 20200925 Pack all boolean attributes into single 32-bit integer for socket read/write. +** CA 20201209 Fixed the default value for the format (short date). */ /* @@ -110,7 +111,7 @@ * The format style constant, it can be one of DTS_LONGDATEFORMAT, DTS_SHORTDATEFORMAT, * DTS_TIMEFORMAT, DTS_CUSTOMFORMAT. */ - public int formatStyle; + public int formatStyle = DTS_SHORTDATEFORMAT; /** Holds the current date time format.*/ public String customFormat; @@ -170,10 +171,10 @@ public int trailingForegroundColor = -1; /** True if a checkbox to the left of the date must be displayed, otherwise false */ - public boolean checkBox; + public boolean checkBox = false; /** Its value indicates if the up and down buttons are used to modify dates or not*/ - public boolean upDown; + public boolean upDown = false; /** Represents an index of the selected field of the date/time content */ public int selectedFieldIndex; === modified file 'src/com/goldencode/p2j/ui/CalendarWidget.java' --- src/com/goldencode/p2j/ui/CalendarWidget.java 2020-09-27 22:30:12 +0000 +++ src/com/goldencode/p2j/ui/CalendarWidget.java 2020-12-09 18:05:56 +0000 @@ -4,9 +4,14 @@ ** ** Copyright (c) 2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description----------------------------------- +** -#- -I- --Date-- --------------------------------------Description----------------------------------------- ** 001 SBI 20200114 Created initial version. ** 002 SBI 20200306 Changed to override getScreenValue as it always returns the valid date/time. +** 003 CA 20201109 Expose the CALENDAR:VALUE as a 'CalendarValue' attribute which follows the 4GL's datetime +** string representation, and not ISO-8601. +** CA 20201119 Rely on CalendarConfig individual fields (month, day, etc) when computing the value for +** CALENDAR:VALUE attribute. +** CA 20201209 Added support for COM event procedures. */ /* @@ -67,6 +72,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeParseException; +import com.goldencode.p2j.comauto.*; import com.goldencode.p2j.util.*; @@ -75,8 +81,37 @@ */ public class CalendarWidget extends ControlEntityExt -implements Calendar +implements Calendar, + EmitsComEvents { + /** A definition of a corresponding COM-OBJECT used to raise the COM events. */ + private ComObject obj = new ComObject() + { + @Override + public void delete() + { + // no-op + } + + @Override + public boolean valid() + { + return CalendarWidget.this.valid(); + } + + @Override + public String getActivexName() + { + return "DTPicker"; + } + + @Override + public String getName() + { + return CalendarWidget.this.name().toStringMessage(); + }; + }; + /** * Creates the instance of the calendar widget. */ @@ -115,8 +150,26 @@ * @return The current date time value as a character value */ @Override - public character getDateTimeValue() + public character getCalendarValue() { + if ("?".equals(config.dateTimeValue)) + { + return new character(); + } + + // the returned string is formatted based on SESSION:DATE-FORMAT followed by the time, in 'hh:mm:ss.sss' + try + { + datetime dt = new datetime(config.month, config.day, config.year, config.hour, config.minute, config.second); + + String res = dt.toStringExport(); + return new character(res); + } + catch (DateTimeParseException ex) + { + // ignore + } + return new character(config.dateTimeValue); } @@ -127,14 +180,24 @@ * The new current date and time value */ @Override - public void setDateTimeValue(datetime value) + public void setCalendarValue(datetime value) { config.dateTimeValue = value.getIsoDate(); + config.year = value.getYear(); + config.month = value.getMonth(); + config.day = value.getDay(); + config.hour = value.getHours(); + config.minute = value.getMinutes(); + config.second = value.getSeconds(); + config.dayOfWeek = value.getWeekday(); setScreenValue(value); - pushWidgetAttr(new String[] {"dateTimeValue", "formatStyle"}, - new Object[] {config.dateTimeValue, config.formatStyle}); + pushWidgetAttr(new String[] {"dateTimeValue", "formatStyle", + "year", "month", "day", "hour", "minute", "second", "dayOfWeek"}, + new Object[] {config.dateTimeValue, config.formatStyle, + config.year, config.month, config.day, + config.hour, config.minute, config.second, config.dayOfWeek}); } /** @@ -144,9 +207,9 @@ * The new current date value */ @Override - public void setDateTimeValue(date value) + public void setCalendarValue(date value) { - setDateTimeValue(new datetime(value)); + setCalendarValue(new datetime(value)); } /** @@ -156,14 +219,14 @@ * The new current date and time value as a character value */ @Override - public void setDateTimeValue(character value) + public void setCalendarValue(character value) { if (value == null || value.isUnknown()) { return; } - setDateTimeValue(value.toJavaType()); + setCalendarValue(value.toJavaType()); } /** @@ -173,13 +236,13 @@ * The new current date and time value representing by a string */ @Override - public void setDateTimeValue(String value) + public void setCalendarValue(String value) { LocalDateTime parsedDate = parseIsoDate(value); if (parsedDate != null) { - setDateTimeValue(new datetime(parsedDate.getMonthValue(), + setCalendarValue(new datetime(parsedDate.getMonthValue(), parsedDate.getDayOfMonth(), parsedDate.getYear(), parsedDate.getHour(), @@ -189,7 +252,7 @@ else { // supposed that value has the current 4GL date time format - setDateTimeValue(new datetime(value)); + setCalendarValue(new datetime(value)); } } @@ -821,4 +884,23 @@ return new character(getAttr("dateTimeValue", () -> config.dateTimeValue)); } + + /** + * The method is called when a COM event is to be emitted by the means of this instance. + * + * @param eventName + * Event name. + * @param args + * Event arguments. + * + * @return The array of all output arguments produced by the event, if any. + */ + @Override + public BaseDataType[] emitComEvent(String eventName, BaseDataType[] args) + { + ComEvent event = new ComEvent(obj, eventName, args); + ErrorManager.nestedSilent(() -> ComServer.emit(event, true)); + + return args; + } } === modified file 'src/com/goldencode/p2j/ui/ClientConfigManager.java' --- src/com/goldencode/p2j/ui/ClientConfigManager.java 2020-09-22 23:33:18 +0000 +++ src/com/goldencode/p2j/ui/ClientConfigManager.java 2020-10-13 06:28:13 +0000 @@ -34,6 +34,7 @@ ** server config managers can now be dynamically extended with new widget ** configurations. ** 021 CA 20200922 Replaced accessIdx map with an array, where the index is the internal field ID. +** HC 20201013 Server-pushed config field changes are serialized with field ids instead of names. */ /* @@ -147,6 +148,28 @@ } /** + * Set the widget config field to the specified value. + * + * @param wid + * The widget ID. + * @param wcfg + * The widget configuration; if this is a down widget, this will be used. + * @param fid + * The config field id. + * @param value + * The list of new config field values. + */ + public void setConfigField(WidgetId wid, WidgetConfig wcfg, int fid, Object value) + { + if (!(wid instanceof WidgetDownId)) + { + wcfg = getActiveConfig(wid); + } + WidgetConfigDef wdef = getConfigDef(wid.asInt()); + setWidgetConfig(wcfg, value, wdef, fid); + } + + /** * Inform the client-side that it can no longer send config updates. */ public void deactivateConfigUpdates() === modified file 'src/com/goldencode/p2j/ui/ClientExports.java' --- src/com/goldencode/p2j/ui/ClientExports.java 2020-09-27 14:46:15 +0000 +++ src/com/goldencode/p2j/ui/ClientExports.java 2020-11-17 20:03:34 +0000 @@ -272,6 +272,8 @@ ** 160 HC 20200726 Initial implementation of SPREADSHEET widget and related changes. ** SBI 20200916 Changed continueEditing to return array of screen buffers or null. ** HC 20200927 Batching of server config changes. +** CA 20201112 Avoid pushing the entire frame definition when attaching a widget at runtime. +** CA 20201117 Added API to delete a widget at runtime. */ /* @@ -1207,6 +1209,29 @@ public String refreshFrameWidget(int fid, int wid, BaseDataType value); /** + * Attach a widget to a frame, at runtime. The widget can be a dynamic widget or a full static frame. + * + * @param fid + * The parent frame's ID. + * @param wid + * The widget ID. + * @param cfg + * The widget's configuration. + */ + public void attachRuntimeWidget(int fid, int wid, WidgetConfig cfg); + + /** + * Delete and detach a widget from a frame, at runtime. The widget can be a dynamic widget or a full + * static frame. + * + * @param fid + * The parent frame's ID. + * @param wid + * The widget ID. + */ + public void deleteDynamicWidget(int fid, int wid); + + /** * Send array of frame/widget definitions to the client. * * @param sd === modified file 'src/com/goldencode/p2j/ui/ColorTable.java' --- src/com/goldencode/p2j/ui/ColorTable.java 2020-09-17 11:46:00 +0000 +++ src/com/goldencode/p2j/ui/ColorTable.java 2021-01-13 21:04:41 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2014-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description----------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 MAG 20140812 Created initial version. ** 002 MAG 20140911 Integrate environment API. ** 003 CA 20140926 Added shared widget configuration support, to allow GUI/ChUI concrete @@ -34,8 +34,9 @@ ** it the base for the TREELIST widget. ** 017 HC 20200313 Javadoc fixes. ** 018 IAS 20200721 EnvironmentColorTable constructor signature changed -** 019 RFB 20200701 The constructor should allow 0 colorRef index to be passed in. +** 019 RFB 20200701 The constructor should allow 0 colorRef index to be passed in. ** RFB 20200909 Redo 20200701 update post-rebase for #4711 +** OM 20201203 Fixed handling of READ/ONLY attributes. */ /* @@ -216,12 +217,11 @@ } /** - * API needed to implement read-only attribute assignment (a 4GL - * "feature"). + * API needed to implement read-only attribute assignment (a 4GL "feature"). * * @param attribute * The attribute's name. - * + * * @see handle#readOnlyError(handle, String) */ public static void readOnlyError(String attribute) @@ -230,6 +230,21 @@ } /** + * API needed to implement read-only attribute assignment (a 4GL "feature"). + * + * @param attribute + * The attribute's name. + * @param expr + * The value which is attempted to be assigned to the read-only attribute. + * + * @see handle#readOnlyError(handle, String, Object) + */ + public static void readOnlyError(String attribute, Object expr) + { + handle.readOnlyError(asHandle(), attribute); + } + + /** * Get the type of its associated handle. * * @return Always return the PSEUDO-WIDGET value. === modified file 'src/com/goldencode/p2j/ui/ComboBoxWidget.java' --- src/com/goldencode/p2j/ui/ComboBoxWidget.java 2020-09-27 22:30:12 +0000 +++ src/com/goldencode/p2j/ui/ComboBoxWidget.java 2021-01-23 18:01:03 +0000 @@ -2,7 +2,7 @@ ** Module : ComboBoxWidget.java ** Abstract : server side combo box widget implementation ** -** Copyright (c) 2005-2019, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 NVS 20051026 @23483 Created initial version. @@ -63,6 +63,15 @@ ** validations should be enforced on the flag. ** HC 20191120 Negative end range for SET-SELECTION must select the whole screen ** value for COMBO-BOX and FILL-IN widgets. +** 035 HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201126 COMBO-BOX's items must honor the data-type and format properly. +** CA 20210111 Fixed SCREEN-VALUE when the LIST-ITEM-PAIRS is set - there are some quirks where +** an empty LABEL or VALUE will force setting the SCREEN-VALUE to that pair's VALUE. +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. +** CA 20210123 FORMAT change must also update the items. +** A single space char " " set to the SCREEN-VALUE will be allowed to select the +** item with an empty string value. */ /* ** This program is free software: you can redistribute it and/or modify @@ -150,6 +159,72 @@ } /** + * Set the widget's DATA-TYPE. This resets its items and also sets the FORMAT and SCREEN-VALUE accordingly. + * + * @param dataType + * New data type name. + */ + @Override + public void setDataType(String dataType) + { + if (!config.dynamic || config.id == null) + { + // the 'config.id == null' test is required for BROWSE combo-box columns + super.setDataType(dataType); + return; + } + + // reset the items + setItems(new ControlSetItem[0], false); + + super.setDataType(dataType); + } + + /** + * Sets the format specification. + * + * @param format + * The format specification. + */ + @Override + public void setFormat(String format) + { + String oldFormat = config.getDynamicFormat(); + if (Utils.equals(oldFormat, format)) + { + return; + } + + super.setFormat(format); + + // re-calculate the items using the new format + boolean pairs = config.pairs != null && config.pairs; + + Class cls = BaseDataType.fromTypeName(config.dataType); + + for (ControlSetItem item : config.items) + { + item.setFormat(config.format); + + BaseDataType value = item.getValue(); + character stringVal = new character(value.toString(config.format)).truncateValue(); + + BaseDataType newValue = GenericFrame.parseScreenValue(cls, stringVal, config.format); + + item.setValue(newValue); + + if (!pairs) + { + item.setLabel(stringVal); + } + } + + checkItems(config.items); + + pushWidgetAttr("items", config.items); + } + + /** * Default constructor. * * @param dynamic @@ -309,7 +384,7 @@ { end = len; } - else if (end > getAttr("widthChars", () -> config.widthChars) - 4) + else if (end > getAttr("widthChars", () -> config.widthChars, true) - 4) { return new logical(false); } @@ -347,7 +422,7 @@ } String s = ""; ControlSetItem[] items = getAttr("items", () -> config.items); - int selectedIndex = getAttr("selectedIndex", () -> config.selectedIndex); + int selectedIndex = getAttr("selectedIndex", () -> config.selectedIndex, true); if (items != null && selectedIndex >= 0 && selectedIndex < items.length) { @@ -355,7 +430,7 @@ s = item.getLabel().toStringMessage(); } - int widthChars = (int) getAttr("widthChars", () -> config.widthChars); + int widthChars = (int) getAttr("widthChars", () -> config.widthChars, true); int selectionStart = getAttr("selectionStart", () -> config.selectionStart); int selectionEnd = getAttr("selectionEnd", () -> config.selectionEnd); @@ -530,11 +605,11 @@ @Override public integer getInnerLines() { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { return new integer(0); } - return new integer(getAttr("innerLines", () -> config.innerLines)); + return new integer(getAttr("innerLines", () -> config.innerLines, true)); } /** @@ -634,6 +709,7 @@ * @param disableAutoZap * true if the option is set to ON. */ + @LegacyAttribute(name = "DISABLE-AUTO-ZAP", setter = true, ignore = true) @Override public void setDisableAutoZap(logical disableAutoZap) { @@ -646,6 +722,7 @@ * @param disableAutoZap * true if the option is set to ON. */ + @LegacyAttribute(name = "DISABLE-AUTO-ZAP", setter = true, ignore = true) @Override public void setDisableAutoZap(boolean disableAutoZap) { @@ -657,6 +734,7 @@ * * @return value of the attribute. */ + @LegacyAttribute(name = "DISABLE-AUTO-ZAP", ignore = true) @Override public logical isDisableAutoZap() { @@ -670,6 +748,7 @@ * @param value * New value for the attribute. */ + @LegacyAttribute(name = "AUTO-ZAP", setter = true, ignore = true) @Override public void setAutoZap(logical value) { @@ -682,6 +761,7 @@ * @param value * New value for the attribute. */ + @LegacyAttribute(name = "AUTO-ZAP", setter = true, ignore = true) @Override public void setAutoZap(boolean value) { @@ -693,6 +773,7 @@ * * @return value of the attribute. */ + @LegacyAttribute(name = "AUTO-ZAP", ignore = true) @Override public logical isAutoZap() { @@ -707,6 +788,7 @@ * @param offset * The character index at which the cursor is to be placed. */ + @LegacyAttribute(name = "CURSOR-OFFSET", setter = true, ignore = true) @Override public void setCursorOffset(double offset) { @@ -720,6 +802,7 @@ * @param offset * The character index at which the cursor is to be placed. */ + @LegacyAttribute(name = "CURSOR-OFFSET", setter = true, ignore = true) @Override public void setCursorOffset(NumberType offset) { @@ -733,6 +816,7 @@ * * @return The character index at which the cursor is placed. */ + @LegacyAttribute(name = "CURSOR-OFFSET", ignore = true) @Override public integer getCursorOffset() { @@ -1020,7 +1104,7 @@ if (index < 1 || index > items.length) return new character(); - return new character(items[(int)index - 1].getValue()); + return new character(items[(int)index - 1].getCharacterValue()); } /** @@ -1053,11 +1137,14 @@ { character r = super.getScreenValue(); + /* + CA: the initial reason for this code is unknown at this time, and contradicts with findings related to + LIST-ITEM-PAIRS initial value in GUI and ChUI. if (ignore(r) && config.items != null && config.items.length > 0) { return new character(config.items[0].getValue()); } - + */ return r; } } @@ -1151,11 +1238,6 @@ if (!LogicalTerminal.isChui()) { super.setListItemPairs(list); - // in GUI after realization COMBO-BOX resets the screen value to unknown here - if (getAttr("realized", () -> config.realized)) - { - setScreenValue(new character()); - } } else { @@ -1164,6 +1246,80 @@ } /** + * Set the current value in the screen buffer of the backing data for + * this widget. If the given value is null then this + * widget will be set to the uninitialized value. + * + * @param value + * The new value for the widget, use null to set + * the value as uninitialized. + */ + @Override + protected void setScreenValueInt(character value) + { + if (!internalScreenValueUsage && !value.isUnknown() && value.getValue().isEmpty()) + { + value = new character(); + } + + super.setScreenValueInt(value); + } + + /** + * Store array of items into config. + * + * @param items + * Array of new items. + * @param pairs + * {@code true} if LIST_ITEM_PAIRS, RADIO_BUTTONS is set false if LIST_ITEMs is set. + * @param checkItems + * If this value is true, then items are checked and truncated. + * @param newAddedItems + * The list of new added items + */ + @Override + protected void setItems(ControlSetItem[] items, + Boolean pairs, + boolean checkItems, + List newAddedItems) + { + // do some house-keeping special for combo-boxes. + // 1. convert the value to the proper data-type + // 2. in case of list-items, the 'label' must be the formatted value + + Class cls = BaseDataType.fromTypeName(config.dataType); + + for (ControlSetItem item : items) + { + item.setFormat(config.format); + + BaseDataType value = item.getValue(); + + BaseDataType newValue = BaseDataType.generateDefault(cls); + newValue.assign(value); + + if (value.getTypeName().equals(config.dataType)) + { + continue; + } + + item.setValue(newValue); + + if (pairs == null || !pairs) + { + item.setLabel(new character(newValue.toString(config.format))); + } + } + + super.setItems(items, pairs, checkItems, newAddedItems); + + if (checkItems) + { + checkItems(items); + } + } + + /** * Remove trailing spaces. * * @param value @@ -1215,9 +1371,11 @@ int itemId = getAttr("itemId", () -> config.itemId); config.itemId = itemId + 1; - return new ControlSetItem(itemId, - label.isUnknown() ? new character(" ") : (character)label.duplicate(), - truncateValue(value.duplicate())); + label = label.isUnknown() ? new character(" ") : (character)label.duplicate(); + ControlSetItem item = new ControlSetItem(itemId, label, truncateValue(value.duplicate())); + item.setFormat(config.format); + + return item; } /** @@ -1343,7 +1501,7 @@ @Override boolean setScreenValue(ScreenBuffer frameBuf, Object value, boolean inUIStmt) { - if (frameBuf.getWidgetValue(getId()) != null) + if (!internalScreenValueUsage || inUIStmt) { if (value == null || (value instanceof BaseDataType && ((BaseDataType) value).isUnknown())) @@ -1351,7 +1509,9 @@ return false; } - if (value instanceof character && ((character) value).toJavaType().length() == 0) + boolean pairs = (config.pairs != null && config.pairs); + if ((!pairs || inUIStmt) && + (value instanceof character) && ((character) value).toJavaType().length() == 0) { return false; } @@ -1391,4 +1551,69 @@ return super.isValidScreenValue(value); } + + /** + * Verify which items needs to be set as SCREEN-VALUE. + * + * @param items + * The items to be set. + */ + private void checkItems(ControlSetItem[] items) + { + boolean pairs = config.pairs != null && config.pairs; + + // empty value has priority over empty label. + // during frame setup, the screen-value is always set to the pair's value, not label (in case of cases + // when they are both empty). + // after frame setup, setting the LIST-ITEM-PAIRS looks only at the label (and *not* the item's value), + // but the screen-value gets set to the item's value. + BaseDataType emptyValue = null; + BaseDataType emptyLabel = null; + + if (pairs) + { + for (ControlSetItem item : items) + { + BaseDataType value = item.getValue(); + + String valTxt = new character(value.toString(config.format)).truncateValue().toJavaType(); + String lblTxt = item.getLabel().truncateValue().toJavaType(); + if (emptyValue == null && valTxt.isEmpty()) + { + emptyValue = value; + } + if (emptyLabel == null && lblTxt.isEmpty()) + { + emptyLabel = value; + } + } + } + + if (!LogicalTerminal.isChui()) + { + if (emptyValue != null && frame.isInsideSetup()) + { + setScreenValueInt(new character(emptyValue.toString(config.format)).truncateValue()); + } + else if (emptyLabel != null) + { + setScreenValueInt(new character(emptyLabel.toString(config.format)).truncateValue()); + } + else if (frame == null || !frame.isInsideSetup()) + { + setScreenValueInt(new character()); + } + } + else if (pairs) + { + if (items.length > 0) + { + setScreenValueInt(new character(items[0].getCharacterValue().truncateValue())); + } + } + else + { + setScreenValueInt(new character(" ")); + } + } } === modified file 'src/com/goldencode/p2j/ui/CommonListWidget.java' --- src/com/goldencode/p2j/ui/CommonListWidget.java 2019-05-08 21:43:48 +0000 +++ src/com/goldencode/p2j/ui/CommonListWidget.java 2020-12-01 12:59:56 +0000 @@ -2,15 +2,16 @@ ** Module : CommonListWidget.java ** Abstract : Define all list widgets-related APIs available via a widget handle instance ** -** Copyright (c) 2015-2019, Golden Code Development Corporation. +** Copyright (c) 2015-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description---------------------------------------- ** 001 IAS 20150209 Created initial version. ** 002 IAS 20150225 Added support for the DELIMITER attribute. ** 003 OM 20170428 Added support for the INSERT method. ** 004 CA 20181221 Moved LIST-ITEMS from CommonListWidget to CommonWidget, as 4GL allows any ** widget to access any attribute, and this was found in customer code. ** 005 CA 20190508 Added ADD-FIRST(x, ). +** 006 CA 20201126 Added ADD-FIRST(x, ) and ADD-FIRST(x, ). */ /* ** This program is free software: you can redistribute it and/or modify @@ -184,6 +185,20 @@ * @return true if the method succeeds. */ @LegacyMethod(name = "ADD-FIRST") + public logical addFirst(String label, boolean value); + + /** + * Implements the ADD-FIRST() widget method, which adds a single label + * and value pair to the beginning of the list. + * + * @param label + * The label of the item to add. + * @param value + * The value of the item to add. + * + * @return true if the method succeeds. + */ + @LegacyMethod(name = "ADD-FIRST") public logical addFirst(String label, int value); /** @@ -198,6 +213,20 @@ * @return true if the method succeeds. */ @LegacyMethod(name = "ADD-FIRST") + public logical addFirst(String label, double value); + + /** + * Implements the ADD-FIRST() widget method, which adds a single label + * and value pair to the beginning of the list. + * + * @param label + * The label of the item to add. + * @param value + * The value of the item to add. + * + * @return true if the method succeeds. + */ + @LegacyMethod(name = "ADD-FIRST") public logical addFirst(character label, String value); /** === modified file 'src/com/goldencode/p2j/ui/CommonWidget.java' --- src/com/goldencode/p2j/ui/CommonWidget.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/CommonWidget.java 2020-10-31 00:42:17 +0000 @@ -92,6 +92,8 @@ ** RFB 20200415 Changed getFrameName getter to return BDT (character) instead of String. ** EVL 20200429 Added new method to get specific widget ID in certain conditions. ** 119 GES 20200627 Moved ROW/COLUMN attributes into Coordinates interface (and renamed that interface). +** 120 VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. +** OM 20201030 Invalid attribute API support for getters/setters. */ /* @@ -444,6 +446,7 @@ * * @return nothing. */ + @LegacyAttribute(name = "MAX-HEIGHT") public decimal getMaxHeightChars(); /** @@ -1986,26 +1989,24 @@ /** * Get the Progress IMAGE attribute * - * @return the name of the image loaded by LOAD-IMAGE or LOAD-IMAGE-UP for BUTTON. + * @return the name of the image loaded by LOAD-IMAGE or LOAD-IMAGE-UP for BUTTON. */ @LegacyAttribute(name = "IMAGE") public character getImage(); - - + /** * Sets the LIST-ITEMS writeable attribute. * * @param list - * A list of delimited items using the configured delimiter for - * this widget (a comma by default) to split the items. These - * items will be set as the full list of items in this widget. + * A list of delimited items using the configured delimiter for this widget (a comma by default) + * to split the items. These items will be set as the full list of items in this widget. */ @LegacyAttribute(name = "LIST-ITEMS", setter = true) public default void setListItems(character list) { - handle.invalidAttribute("LIST-ITEMS", new handle(this)); + handle.invalidAttribute("LIST-ITEMS", true, getResourceType().toStringMessage()); } - + /** * Setter for the LIST-ITEMS writeable attribute. * @@ -2015,9 +2016,9 @@ @LegacyAttribute(name = "LIST-ITEMS", setter = true) public default void setListItems(String list) { - handle.invalidAttribute("LIST-ITEMS", new handle(this)); + handle.invalidAttribute("LIST-ITEMS", true, getResourceType().toStringMessage()); } - + /** * Gets the LIST-ITEMS writeable attribute. * @@ -2026,10 +2027,10 @@ @LegacyAttribute(name = "LIST-ITEMS") public default character getListItems() { - handle.invalidAttribute("LIST-ITEMS", new handle(this)); + handle.invalidAttribute("LIST-ITEMS", false, getResourceType().toStringMessage()); return new character(); } - + /** * Sets the READ-ONLY attribute of the widget. * === modified file 'src/com/goldencode/p2j/ui/CommonWindow.java' --- src/com/goldencode/p2j/ui/CommonWindow.java 2019-11-26 22:20:19 +0000 +++ src/com/goldencode/p2j/ui/CommonWindow.java 2020-12-25 16:37:53 +0000 @@ -22,6 +22,7 @@ ** 012 HC 20171027 Extended DISABLE-REDRAW to all widget types. Previously only WINDOW widget ** was supported. ** 013 HC 20191126 Implemented RESIZE attribute for DIALOG-BOX. This is an extension to 4GL. +** 014 SVL 20201225 MIN-HEIGHT-CHARS moved to MinHeightCharsInterface. */ /* ** This program is free software: you can redistribute it and/or modify @@ -215,14 +216,6 @@ public integer getMaxWidthPixels(); /** - * Gets the MIN-HEIGHT-CHARS writable attribute. - * - * @return current value of MIN-HEIGHT-CHARS attribute - */ - @LegacyAttribute(name = "MIN-HEIGHT-CHARS") - public decimal getMinHeightChars(); - - /** * Gets the MIN-HEIGHT-PIXELS writable attribute. * * @return current value of MIN-HEIGHT-PIXELS attribute @@ -420,15 +413,6 @@ public void setMaxWidthPixels(integer max); /** - * Sets the MIN-HEIGHT-CHARS writable attribute. - * - * @param min - * The new value for the MIN-HEIGHT-CHARS attribute. - */ - @LegacyAttribute(name = "MIN-HEIGHT-CHARS", setter = true) - public void setMinHeightChars(NumberType min); - - /** * Sets the MIN-HEIGHT-PIXELS writable attribute. * * @param min === modified file 'src/com/goldencode/p2j/ui/ConfigManager.java' --- src/com/goldencode/p2j/ui/ConfigManager.java 2020-09-27 18:16:32 +0000 +++ src/com/goldencode/p2j/ui/ConfigManager.java 2020-10-13 06:28:13 +0000 @@ -45,6 +45,8 @@ ** HC 20200924 Enabled config fields tracking. Also added system property to allow to disable this ** feature. ** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class. +** CA 20201011 Small performance improvement in addDirtyConfig - exit early if the field is null. +** HC 20201013 Server-pushed config field changes are serialized with field ids instead of names. */ /* ** This program is free software: you can redistribute it and/or modify @@ -273,6 +275,20 @@ } /** + * Get the {@link WidgetConfigDef} instance associated with the given {@link WidgetConfig} + * implementation. + * + * @param cls + * The widget config class. + * + * @return See above. + */ + public WidgetConfigDef getConfigDef(Class cls) + { + return cfgDefs.get(cls); + } + + /** * The method attempts to resolve a {@link ConfigOwner} reference from * widgetId. * @@ -295,20 +311,6 @@ protected abstract boolean isRegisteredWidget(WidgetId id); /** - * Get the {@link WidgetConfigDef} instance associated with the given {@link WidgetConfig} - * implementation. - * - * @param cls - * The widget config class. - * - * @return See above. - */ - protected WidgetConfigDef getConfigDef(Class cls) - { - return cfgDefs.get(cls); - } - - /** * Determine if the specified config instance can be tracked. *

* The config can be tracked if tracking of the config fields is active at this moment and @@ -349,7 +351,7 @@ */ public void addDirtyConfig(WidgetConfig cfg, String field) { - if (!trackConfig(cfg)) + if (field == null || !trackConfig(cfg)) { return; } @@ -360,10 +362,7 @@ dirtyConfigs.put(cfg.id, fields = new HashSet<>()); } - if (field != null) - { - fields.add(field); - } + fields.add(field); } /** === modified file 'src/com/goldencode/p2j/ui/ConfigSyncManager.java' --- src/com/goldencode/p2j/ui/ConfigSyncManager.java 2018-01-04 18:21:14 +0000 +++ src/com/goldencode/p2j/ui/ConfigSyncManager.java 2020-10-11 16:07:42 +0000 @@ -2,14 +2,15 @@ ** Module : ConfigSyncManager.java ** Abstract : Implements synchronization of config changes. ** -** Copyright (c) 2015-2018, Golden Code Development Corporation. +** Copyright (c) 2015-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description----------------------------------- +** -#- -I- --Date-- --------------------------------------Description----------------------------------------- ** 001 HC 20150323 Created initial version ** 002 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10. ** 003 IAS 20160331 Fixed typo in the markScopeEnd() method ** 004 ECF 20171122 Minor optimization in registerConfig. ** 005 ECF 20180101 Minimize thread-local access to improve performance. +** 006 CA 20201011 State.configOwnerMap and pendingConfigs are now identity maps. */ /* ** This program is free software: you can redistribute it and/or modify @@ -214,10 +215,10 @@ ConfigManager cm = null; Map> ownerMap = state.configOwnerMap; - state.configOwnerMap = new HashMap>(); + state.configOwnerMap = new IdentityHashMap>(); Map configs = state.pendingConfigs; - state.pendingConfigs = new HashMap(); + state.pendingConfigs = new IdentityHashMap(); for (Map.Entry c : configs.entrySet()) { @@ -300,7 +301,7 @@ { Map configs = state.pendingConfigs; - if (!configs.containsKey(config)) + if (configs.isEmpty() || !configs.containsKey(config)) { state.duplicateInProgress = true; @@ -326,10 +327,10 @@ int scopeDepth = 0; /** Map of config references and their respective owners. **/ - Map> configOwnerMap = new HashMap<>(); + Map> configOwnerMap = new IdentityHashMap<>(); /** Map of configs modified in the current sync scope. **/ - Map pendingConfigs = new HashMap<>(); + Map pendingConfigs = new IdentityHashMap<>(); /** Flag to prevent recursion during config duplication. **/ boolean duplicateInProgress = false; === modified file 'src/com/goldencode/p2j/ui/ControlEntity.java' --- src/com/goldencode/p2j/ui/ControlEntity.java 2020-09-27 22:30:12 +0000 +++ src/com/goldencode/p2j/ui/ControlEntity.java 2021-01-13 21:04:41 +0000 @@ -2,9 +2,9 @@ ** Module : ControlEntity.java ** Abstract : server side control widget parent class ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- ----------------Description----------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23486 Created initial version. ** 002 GES 20060114 @23908 Removed totally useless screen-value attr. ** 003 SIY 20060202 @24235 Added support for TO option. @@ -74,13 +74,28 @@ ** dialogs for validation errors. ** 044 CA 20180130 Replaced pushScreenDefinition() with pushWidgetAttr() for cases when ** business logic is assigning a widget attribute. -** 045 ECF 20180403 Removed UnimplementedFeature.todo calls in is/setModified. +** 045 ECF 20180403 Removed UnimplementedFeature. TODO calls in is/setModified. ** 046 HC 20180628 Added side-labels to the widget chain. ** 047 CA 20180723 The side-label must know its associated widget (to be able to access ** or set the SCREEN-VALUE attribute). ** 048 VVT 20200203 Class declaration changed to abstract. ** Missing @Override annotation(s) added. +** 049 EVL 20201022 Optimized attributes flush implementation. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** RFB 20201106 Renamed TextBasedWidget from sideLabel to sideLabelWidget since there is already +** a boolean named sideLabel in the instance. This helps avoid any confusion. #4873 +** CA 20201117 Delete the static side-label widget, too, when deleting the widget. +** CA 20201126 Fixed error message for DATA-TYPE setter (widget's type must be used). +** CA 20210122 Fixed side-label problems when they are set via a dynamic widget and +** SIDE-LABEL-HANDLE. +** CA 20210223 Update the SCREEN-VALUE when FORMAT is changed. +** The widgets LABEL (in its config) must be set before the config is updated, to +** keep it in sync with the client-side. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. +** OM 20201231 ErrorManager.displayError() API change. */ + /* ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU Affero General Public License as @@ -171,7 +186,7 @@ * Reference to the side-label widget (as returned by the SIDE-LABEL-HANDLE). When * null, it means no side-label exists (thus SIDE-LABEL-HANDLE is unknown). */ - private LiteralWidget sideLabel = null; + private TextBasedWidget sideLabelWidget = null; /** * Constructor for use by sub-classes. @@ -331,7 +346,7 @@ double col = ConfigHelper.getColon(config) + 1; return new decimal(col); } - + /** * Sets new value of DATA-TYPE attribute. * @@ -343,9 +358,9 @@ { if (dataType == null || dataType.isUnknown()) { - String txt = - "**Attribute DATA-TYPE for the FILL-IN widget has an invalid value of UNKNOWN. (4056)"; - ErrorManager.recordOrShowError(4056, txt, false, false, false); + ErrorManager.recordOrShowError(4056, type()); + // ** Attribute DATA-TYPE for the widget has an invalid value of UNKNOWN. + return; } setDataType(dataType.toStringMessage()); @@ -366,11 +381,25 @@ } config.dataType = dataType; + dataTypeClass = null; // resolve the class now. getDataClass(); pushWidgetAttr("dataType", config.dataType); + + // update the format + Class cls = BaseDataType.fromTypeName(dataType); + BaseDataType defVal = BaseDataType.generateDefault(cls); + if (hasFormat()) + { + setFormat(defVal.defaultFormatString()); + } + + if (frame == null || !frame.isInsideSetup()) + { + setScreenValueInt(new character(defVal)); + } } /** @@ -393,7 +422,7 @@ @Override public logical isModified() { - return new logical(getAttr("modified", () -> config.modified)); + return new logical(getAttr("modified", () -> config.modified, true)); } /** @@ -610,7 +639,7 @@ @Override public handle getSideLabelHandle() { - return sideLabel == null ? new handle() : new handle(sideLabel); + return sideLabelWidget == null ? new handle() : new handle(sideLabelWidget); } /** @@ -647,7 +676,7 @@ @Override public GenericWidget firstChild() { - return sideLabel == null ? this : sideLabel; + return sideLabelWidget == null ? this : sideLabelWidget; } /** @@ -670,9 +699,63 @@ @Override public handle getPrevSibling() { - return sideLabel != null ? new handle(sideLabel) : super.getPrevSibling(); - } - + return sideLabelWidget != null ? new handle(sideLabelWidget) : super.getPrevSibling(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setSideLabelHandle(handle label) + { + if (label.isUnknown() || !validateLabelAssignment()) + { + return; + } + + // set the 'labels' attribute, too + config.labels = !label.isUnknown(); + + if (sideLabelWidget != null) + { + sideLabelWidget.clearSideLabel(); + } + + sideLabelWidget = (TextBasedWidget) label.getResource(); + config.sideLabelId = sideLabelWidget.config.id.asInt(); + setLabelInt(sideLabelWidget.getScreenValue().toStringMessage()); + sideLabelWidget.makeSideLabel(this); + + ConfigManager cm = ConfigManager.getInstance(); + if (cm.getActiveConfig(sideLabelWidget.config.id) == null) + { + cm.addWidgetConfig(sideLabelWidget.config()); + } + } + + /** + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + super.delete(); + + if (sideLabelWidget != null && !sideLabelWidget._dynamic()) + { + sideLabelWidget.delete(); + } + } + /** * Sets the value for the FONT attribute. * Only valid width values must be passed in the font argument. @@ -685,7 +768,7 @@ { super.changeFontWorker(font); - if (getAttr("autoResize ", () -> config.autoResize )&& frame != null && !frame.isInsideSetup()) + if (getAttr("autoResize", () -> config.autoResize )&& frame != null && !frame.isInsideSetup()) { config.fontChanged = true; // let the width compute automatically @@ -773,6 +856,11 @@ */ protected boolean hasSideLabelHandle() { + if (sideLabelWidget != null && sideLabelWidget._dynamic()) + { + return true; + } + FrameConfig fconfig = frame.getFrameWidget().config; // options which affect this: @@ -793,7 +881,7 @@ /** * Process the side-label for this widget. *

- * If the widget accepts a side-label, it will construct it if {@link #sideLabel} is not + * If the widget accepts a side-label, it will construct it if {@link #sideLabelWidget} is not * already set. * * @param explicit @@ -811,36 +899,40 @@ // do this only once. nothing is initialized here. // all info will be coming from the client-side - if (sideLabel == null) + if (sideLabelWidget == null) { if (getAttr("sideLabelId", () -> config.sideLabelId) == -1) { - sideLabel = LiteralWidget.createSideLabel(this); - sideLabel.config.widthChars = -1; - sideLabel.config.align = ControlEntity.ALIGN_RIGHT; - sideLabel.setFrame(ControlEntity.this.getFrame()); + sideLabelWidget = LiteralWidget.createSideLabel(this); + sideLabelWidget.config.widthChars = -1; + sideLabelWidget.config.align = ControlEntity.ALIGN_RIGHT; + sideLabelWidget.setFrame(ControlEntity.this.getFrame()); config.sideLabelId = WidgetId.nextID().asInt(); - sideLabel.config.id = new WidgetId(config.sideLabelId); + sideLabelWidget.config.id = new WidgetId(config.sideLabelId); - LogicalTerminal.registerWidget(sideLabel, config.sideLabelId); - ConfigManager.getInstance().addWidgetConfig(sideLabel.config); + LogicalTerminal.registerWidget(sideLabelWidget, config.sideLabelId); + ConfigManager.getInstance().addWidgetConfig(sideLabelWidget.config); } else { // someone else has created it (i.e. a shared frame), retrieve it from the registry - sideLabel = (LiteralWidget) LogicalTerminal.getWidgetForId(getAttr("sideLabelId", () -> config.sideLabelId)); + sideLabelWidget = (LiteralWidget) LogicalTerminal.getWidgetForId(getAttr("sideLabelId", () -> config.sideLabelId)); } } if (explicit) { - sideLabel.config.align = ControlEntity.ALIGN_LEFT; + sideLabelWidget.config.align = ControlEntity.ALIGN_LEFT; } // we are just updating the screen value, we don't need to evaluate header expressions // or other dynamic content - frame.getFrameBufferRaw().putScreenValue(getAttr("sideLabelId", () -> config.sideLabelId), new character(getAttr("label", () -> config.label))); + int lblId = getAttr("sideLabelId", () -> config.sideLabelId); + character lbl = new character(getAttr("label", () -> config.label)); + ScreenBuffer frameBuf = frame.getFrameBufferRaw(); + frameBuf.putScreenValue(lblId, lbl); + frameBuf.putWidgetValue(lblId, lbl); } return getAttr("sideLabelId", () -> config.sideLabelId); @@ -861,7 +953,7 @@ @Override protected logical validateFields(boolean enabledOnly) { - if (enabledOnly && !getAttr("enabled", () -> config.enabled)) + if (enabledOnly && !getAttr("enabled", () -> config.enabled, true)) { return new logical(true); } @@ -901,9 +993,9 @@ { return new logical(true); } - else if (getAttr("enabled", () -> config.enabled)) + else if (getAttr("enabled", () -> config.enabled, true)) { - ErrorManager.displayError(validationError, true); + ErrorManager.displayError(validationError, true, null); } return new logical(false); === modified file 'src/com/goldencode/p2j/ui/ControlEntityExt.java' --- src/com/goldencode/p2j/ui/ControlEntityExt.java 2020-09-27 22:30:12 +0000 +++ src/com/goldencode/p2j/ui/ControlEntityExt.java 2020-10-22 14:25:52 +0000 @@ -9,6 +9,7 @@ ** 002 SBI 20200304 Merged OcxDragDropImpl to ControlEntityExt. ** 003 EVL 20200428 Added implementation for setComData(). ** 005 SBI 20200810 Fixed compilation issue due to OcxDragDrop was cleaned. +** 006 EVL 20201022 Optimized attributes flush implementation. */ /* @@ -244,7 +245,7 @@ @Override public void setMouseIcon(String value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { if (config.mouseIcon == value) { @@ -497,7 +498,7 @@ */ public void setOleX(double value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { if (config.oleX == value) { @@ -554,7 +555,7 @@ */ public void setOleY(double value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { if (config.oleY == value) { === modified file 'src/com/goldencode/p2j/ui/ControlFrameWidget.java' --- src/com/goldencode/p2j/ui/ControlFrameWidget.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/ControlFrameWidget.java 2020-11-17 20:03:34 +0000 @@ -15,6 +15,7 @@ ** 006 VVT 20200213 getActiveX() fixed: the empty name case is now handled ** 007 VVT 20200831 ControlFrameComObjectImpl.valid() overloaded method removed: the base method ** returning true unconditionally is used instead. This complies with the 4gl. +** 008 CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. */ /* @@ -139,23 +140,16 @@ /** * Delete the resource. - * - * @return true if the resource was deleted. */ @Override - protected boolean resourceDelete() + public void delete() { - if (!super.resourceDelete()) - { - return false; - } - - if (comObject != null) - { + if (resourceDelete()) + { + super.delete(); + comObject.destroy(); } - - return true; } /** === modified file 'src/com/goldencode/p2j/ui/ControlSetEntity.java' --- src/com/goldencode/p2j/ui/ControlSetEntity.java 2020-09-28 10:09:11 +0000 +++ src/com/goldencode/p2j/ui/ControlSetEntity.java 2021-01-28 11:45:06 +0000 @@ -2,7 +2,7 @@ ** Module : ControlSetEntity.java ** Abstract : server side grouped control widget parent class ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- ** 001 NVS 20051026 @23487 Created initial version. @@ -75,6 +75,15 @@ ** initial item list. ** 045 IAS 20200306 Fix for LOOKUP method implementation ** 046 IAS 20200320 Fixed LIST-ITEMS attribute support +** 047 EVL 20201022 Optimized attributes flush implementation. +** CA 20201126 Added ADD-FIRST(x, ) and ADD-FIRST(x, ). +** COMBO-BOX's items must honor the DATA-TYPE and FORMAT. +** VVT 20201215 Fixed: getListItems() must trim item trailing spaces, but not trim +** result leading spaces. (See #5040) +** CA 20201221 Additional fix for VVT/20201215 to fix getListItemPairs() in the same way. +** CA 20210111 LIST-ITEM-PAIRS attribute set will change the screen-value only in ChUI mode. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. */ /* ** This program is free software: you can redistribute it and/or modify @@ -396,6 +405,23 @@ * @return true if the method succeeds. */ @Override + public logical addFirst(String label, boolean value) + { + return addFirst(new character(label), new logical(value)); + } + + /** + * Implements the ADD-FIRST() widget method, which adds a single label + * and value pair to the beginning of the list. + * + * @param label + * The label of the item to add. + * @param value + * The value of the item to add. + * + * @return true if the method succeeds. + */ + @Override public logical addFirst(String label, int value) { return addFirst(new character(label), new integer(value)); @@ -413,6 +439,23 @@ * @return true if the method succeeds. */ @Override + public logical addFirst(String label, double value) + { + return addFirst(new character(label), new decimal(value)); + } + + /** + * Implements the ADD-FIRST() widget method, which adds a single label + * and value pair to the beginning of the list. + * + * @param label + * The label of the item to add. + * @param value + * The value of the item to add. + * + * @return true if the method succeeds. + */ + @Override public logical addFirst(String label, String value) { return addFirst(new character(label), new character(value)); @@ -1451,7 +1494,20 @@ setItems(new ControlSetItem[0], false); - addFirst(list); + boolean saveRealize = this.realizeOnAttributeAccess; + this.realizeOnAttributeAccess = false; + try + { + addFirst(list); + } + finally + { + if (LogicalTerminal.isChui()) + { + setScreenValueInt(new character()); + } + this.realizeOnAttributeAccess = saveRealize; + } } /** @@ -1509,7 +1565,7 @@ return; } - setListItemPairsWorker(list, getAttr("realized", () -> config.realized)); + setListItemPairsWorker(list, getAttr("realized", () -> config.realized, true)); } /** @@ -1537,18 +1593,21 @@ StringBuilder sb = new StringBuilder(); + final char delimiter = getAttr("delimiter", () -> config.delimiter); for (int i = 0; i < items.length; i++) { - sb.append(items[i].getLabel().toStringMessage()); + /** + * OE truncates trailing spaces for each item + */ + sb.append(items[i].getLabel().truncateValue().toStringMessage()); if (i < items.length - 1) { - sb.append(getAttr("delimiter", () -> config.delimiter)); + sb.append(delimiter); } } - // OpenEdge trims the resulting value - return new character(sb.toString().trim()); + return new character(sb.toString()); } /** @@ -1575,9 +1634,9 @@ for (int i = 0; i < items.length; i++) { - sb.append(items[i].getLabel().toStringMessage()); + sb.append(items[i].getLabel().truncateValue().toStringMessage()); sb.append(delimiter); - sb.append(items[i].getValue().toStringMessage()); + sb.append(items[i].getCharacterValue().truncateValue().toStringMessage()); if (i < items.length - 1) { sb.append(delimiter); @@ -2334,7 +2393,13 @@ { continue; } - newlist.add(new ControlSetItem(config.itemId++, label, value)); + + ControlSetItem item = new ControlSetItem(config.itemId++, label, value); + if (hasFormat()) + { + item.setFormat(config.format); + } + newlist.add(item); } // the format is not used to render list item pairs @@ -2346,9 +2411,9 @@ setItems(newlist.toArray(new ControlSetItem[newlist.size()]), true); // initialize the screen value by the first item - if (initScreenValue) + if (initScreenValue && LogicalTerminal.isChui()) { - setScreenValue(new character(elem[1])); + setScreenValueInt(new character(elem[1])); } } @@ -2446,9 +2511,13 @@ */ protected ControlSetItem controlSetItem(character label, BaseDataType value) { - return new ControlSetItem(config.itemId++, - label.isUnknown() ? new character(" ") : (character)label.duplicate(), - value.duplicate()); + label = label.isUnknown() ? new character(" ") : (character)label.duplicate(); + ControlSetItem item = new ControlSetItem(config.itemId++, label, value.duplicate()); + if (hasFormat()) + { + item.setFormat(config.format); + } + return item; } /** @@ -2775,7 +2844,8 @@ ControlSetItem[] items = getAttr("items", () -> config.items); return Boolean.TRUE.equals(pairs) ? Boolean.TRUE : - !getAttr("itemsSetWhenRealized", () -> config.itemsSetWhenRealized) && !getAttr("realized", () -> config.realized) && + !getAttr("itemsSetWhenRealized", () -> config.itemsSetWhenRealized) && + !getAttr("realized", () -> config.realized, true) && items != null && items.length > 0 ? pairs : null; } === modified file 'src/com/goldencode/p2j/ui/ControlSetItem.java' --- src/com/goldencode/p2j/ui/ControlSetItem.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/ControlSetItem.java 2021-01-28 11:45:06 +0000 @@ -2,17 +2,22 @@ ** Module : ControlSetItem.java ** Abstract : Label-value pair container. ** -** Copyright (c) 2006-2020, Golden Code Development Corporation. -** -** -#- -I- --Date-- -T- --JPRM-- ----------------Description----------------- -** 001 SIY 20060205 NEW @24502 Created initial version -** 002 IAS 20150205 Added id and selectable fields -** 003 IAS 20150212 Renamed 'selectable' field to 'enabled' to reflect semantics -** -** 004 HC 20150323 Removed business logic from config classes. -** 005 SBI 20180831 Added getCharacterValue(). -** 006 SBI 20190322 Added implementation of equals and hashCode. -** 007 IAS 20200823 Rework (de)serialization. +** Copyright (c) 2006-2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- +** 001 SIY 20060205 @24502 Created initial version +** 002 IAS 20150205 Added id and selectable fields +** 003 IAS 20150212 Renamed 'selectable' field to 'enabled' to reflect semantics +** 004 HC 20150323 Removed business logic from config classes. +** 005 SBI 20180831 Added getCharacterValue(). +** 006 SBI 20190322 Added implementation of equals and hashCode. +** 007 IAS 20200823 Rework (de)serialization. +** CA 20201126 Added format, to allow a COMBO-BOX to properly report the value. +** CA 20210123 The characterValue must use the item's format, and not the message-like string. +** This needs to be in sync with GenericFrame.parseScreenValue, as that now uses +** the default format, if the format is not set. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. */ /* ** This program is free software: you can redistribute it and/or modify @@ -97,6 +102,9 @@ /** Item is enabled for RADIO-SET */ private boolean enabled = true; + /** The value's format. */ + private String format = null; + /** * Default constructor * @@ -135,7 +143,18 @@ { this(0, label, value); } - + + /** + * Set the value's format, as reported by {@link #getCharacterValue()}. + * + * @param format + * The value's format. + */ + public void setFormat(String format) + { + this.format = format; + } + /** * Get item label. * @@ -223,11 +242,13 @@ /** * Get the character representation of its value. * - * @return The character representation of its value. + * @return The character representation of its value, honoring any set {@link #format}. */ public character getCharacterValue() { - return new character(getValue().toStringMessage()); + // use the default format here + return format == null ? new character(getValue().toStringMessage()) + : new character(getValue().toString(format)); } /** @@ -298,6 +319,7 @@ writeBaseDataType(out, label); writeBaseDataType(out, value); out.writeBoolean(enabled); + writeString(out, format); } /** @@ -320,5 +342,6 @@ label = readBaseDataType(in); value = readBaseDataType(in); enabled = readBoolean(in); + format = readString(in); } } === modified file 'src/com/goldencode/p2j/ui/ControlTextWidget.java' --- src/com/goldencode/p2j/ui/ControlTextWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/ControlTextWidget.java 2021-01-15 20:55:46 +0000 @@ -2,7 +2,7 @@ ** Module : ControlTextWidget.java ** Abstract : server side text widget implementation ** -** Copyright (c) 2005-2018, Golden Code Development Corporation. +** Copyright (c) 2005-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 NVS 20051026 @23488 Created initial version. @@ -50,6 +50,8 @@ ** 024 OM 20171122 Used WriteProtectable interface for READ-ONLY attribute. ** 025 CA 20180130 Replaced pushScreenDefinition() with pushWidgetAttr() for cases when ** business logic is assigning a widget attribute. +** 026 HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. */ /* @@ -148,6 +150,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) @Override public void setReadOnly(logical r) { === modified file 'src/com/goldencode/p2j/ui/EditorWidget.java' --- src/com/goldencode/p2j/ui/EditorWidget.java 2020-09-28 10:09:11 +0000 +++ src/com/goldencode/p2j/ui/EditorWidget.java 2020-10-22 14:25:52 +0000 @@ -59,6 +59,9 @@ ** Adding EDIT-CAN-UNDO, EDIT-CAN-PASTE support. ** 039 AIL 20191024 EditorWidget always ignores the implicit formats. ** 040 GES 20200213 Added AUTO-INDENT. +** 041 HC 20201010 Implemented selective config flushing. +** GES 20201018 Simplified the code to eliminate an extra call to the screen-buffer. +** EVL 20201022 Optimized attributes flush implementation. */ /* @@ -160,7 +163,7 @@ * beginning. */ public static final int FIND_SELECT = 32; - + /** * Default constructor. */ @@ -281,7 +284,7 @@ */ public integer getInnerLines() { - return new integer(getAttr("innerLines", () -> config.innerLines)); + return new integer(getAttr("innerLines", () -> config.innerLines, true)); } /** @@ -338,7 +341,7 @@ */ public integer getInnerChars() { - return new integer(getAttr("innerChars", () -> config.innerChars)); + return new integer(getAttr("innerChars", () -> config.innerChars, true)); } /** @@ -389,7 +392,7 @@ @Override public void setMaxChars(NumberType chars) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -1060,7 +1063,7 @@ @Override public integer getCursorOffset() { - return new integer(getAttr("cursorOffset", () -> config.cursorOffset)); + return new integer(getAttr("cursorOffset", () -> config.cursorOffset, true)); } /** @@ -1157,7 +1160,7 @@ @Override public void setReturnInserted(boolean value) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -1213,7 +1216,7 @@ @Override public void setWordWrap(final boolean value) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -1278,7 +1281,7 @@ @Override public void setBox(boolean value) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -1992,14 +1995,13 @@ { ((character) value).assign(""); } - - int widgetID = getId(); - if (frameBuf.isIdValid(widgetID)) + + // do not call super method, because [modified] should not be set, instead put the value directly + // into the screen-buffer + if (frameBuf.putWidgetValue(getId(), value)) { - // do not call super method, because [modified] should not be set, instead put the value - // directly: - frameBuf.putWidgetValue(widgetID, value); - // update the [modified] flag. The UI stmts will reset it back to [false] + // the widget is valid in the screen-buffer, now update the [modified] flag; the UI stmts will + // reset it back to [false] config.modified = !inUIStmt; } === modified file 'src/com/goldencode/p2j/ui/EventDefinition.java' --- src/com/goldencode/p2j/ui/EventDefinition.java 2020-10-04 20:14:00 +0000 +++ src/com/goldencode/p2j/ui/EventDefinition.java 2021-01-18 17:53:57 +0000 @@ -3,7 +3,7 @@ ** Abstract : represents events and widgets that determine the condition of ** termination of WAIT-FOR statements and/or triggers ** -** Copyright (c) 2007-2020, Golden Code Development Corporation. +** Copyright (c) 2007-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------- ** 001 NVS 20070112 @31882 Created initial version. @@ -55,6 +55,11 @@ ** CA 20200930 Change 'difference' as it always can return at most 2 values. ** GES 20201001 Fix for global anywhere flag. ** EVL 20201003 Fix for getRowDisplayEvents(). +** CA 20201010 Use a fast-access bitmap instead of set for widget and resource IDs. +** CA 20201117 Fixed a typo in toString(). +** SVL 20201219 Added OF BROWSE br ANYWHERE lookup level. +** EVL 20201222 Adding NPE protection for lookup code. +** CA 20210118 Refactored to rely on the event IDs instead of event names. */ /* @@ -115,6 +120,11 @@ import static com.goldencode.util.NativeTypeSerializer.*; import java.io.*; import java.util.*; +import java.util.function.Consumer; + +import com.goldencode.p2j.ui.chui.*; +import org.roaringbitmap.*; +import org.roaringbitmap.longlong.*; import com.goldencode.p2j.util.*; import com.goldencode.p2j.ui.client.*; @@ -188,23 +198,23 @@ /** Bitwise mask for the global anywhere flag. */ private static final byte MASK_GLOBAL_ANYWHERE = (byte) 0x80; - /** The event name associated with this instance IF and ONLY IF there is only 1 event. */ - private String event = null; + /** The event ID associated with this instance IF and ONLY IF there is only 1 event. */ + private Integer event = null; - /** Set of event names associated with this instance IF AND ONLY IF there is more than one event. */ - private Set events = null; + /** Set of event IDs associated with this instance IF AND ONLY IF there is more than one event. */ + private RoaringBitmap events = null; /** The widget associated with this instance IF and ONLY IF there is only 1 widget. */ private int widget = WidgetId.INVALID_WIDGET_ID; /** Set of widget IDs associated with this instance IF AND ONLY IF there is more than one widget. */ - private Set widgets = null; + private RoaringBitmap widgets = null; /** The resource associated with this instance IF and ONLY IF there is only 1 resource. */ private long resource = ResourceIdHelper.INVALID_RESOURCE; /** Set of resources (or not-attached widget) IF AND ONLY IF there is more than one resource. */ - private Set resources = null; + private Roaring64Bitmap resources = null; /** Widget-specific ANYWHERE option, if given. */ private boolean anywhere = false; @@ -234,7 +244,30 @@ * @param anywhere * true if these events are defined as ANYWHERE */ - public EventDefinition(Set events, Set widgets, Set resources, boolean anywhere) + public EventDefinition(Set events, RoaringBitmap widgets, Roaring64Bitmap resources, boolean anywhere) + { + this.events = new RoaringBitmap(); + this.widgets = widgets; + this.resources = resources; + this.anywhere = anywhere; + this.globalAnywhere = (widgets == null && resources == null); + + events.forEach(evt -> this.events.add(EventList.eventCode(evt).intValue())); + } + + /** + * A constructor. + * + * @param events + * a bitmap of event IDs + * @param widgets + * a Set of widget IDs + * @param resources + * a Set of non-widget resource IDs + * @param anywhere + * true if these events are defined as ANYWHERE + */ + public EventDefinition(RoaringBitmap events, RoaringBitmap widgets, Roaring64Bitmap resources, boolean anywhere) { this.events = events; this.widgets = widgets; @@ -247,7 +280,7 @@ * A constructor. * * @param event - * an event name + * an event ID * @param widget * a widget ID * @param resource @@ -255,7 +288,7 @@ * @param anywhere * true if these events are defined as ANYWHERE */ - public EventDefinition(String event, Integer widget, Long resource, boolean anywhere) + public EventDefinition(Integer event, Integer widget, Long resource, boolean anywhere) { if (widget != null) { @@ -277,7 +310,7 @@ * A constructor. * * @param events - * The event names. + * The event IDs. * @param widget * a widget ID * @param resource @@ -285,7 +318,7 @@ * @param anywhere * true if these events are defined as ANYWHERE */ - public EventDefinition(Set events, Integer widget, Long resource, boolean anywhere) + public EventDefinition(RoaringBitmap events, Integer widget, Long resource, boolean anywhere) { if (widget != null) { @@ -307,7 +340,7 @@ * A constructor. * * @param event - * The event name. + * The event ID. * @param widgets * a Set of widget IDs * @param resources @@ -315,7 +348,7 @@ * @param anywhere * true if these events are defined as ANYWHERE */ - public EventDefinition(String event, Set widgets, Set resources, boolean anywhere) + public EventDefinition(Integer event, RoaringBitmap widgets, Roaring64Bitmap resources, boolean anywhere) { this.event = event; this.widgets = widgets; @@ -349,9 +382,9 @@ * @param widgetId * The widget ID. * @param event - * The event name. + * The event ID. */ - EventDefinition(boolean anywhere, Integer widgetId, String event) + EventDefinition(boolean anywhere, Integer widgetId, Integer event) { this.event = event; this.widget = widgetId; @@ -366,9 +399,9 @@ * @param resId * The resource ID. * @param event - * The event name. + * The event ID. */ - EventDefinition(boolean anywhere, Long resId, String event) + EventDefinition(boolean anywhere, Long resId, Integer event) { this.event = event; this.resource = resId; @@ -379,9 +412,9 @@ * Create a new global ANYWHERE event definition for the specified event. * * @param event - * The event name. + * The event ID. */ - EventDefinition(String event) + EventDefinition(Integer event) { this.event = event; this.globalAnywhere = true; @@ -408,7 +441,25 @@ return universal; } - + + /** + * Gather all the event codes in the given bitmap. + * + * @param eventCodes + * The map where to add all event codes. + */ + public void gatherEventCodes(RoaringBitmap eventCodes) + { + if (event != null) + { + eventCodes.add(event); + } + if (events != null && !events.isEmpty()) + { + eventCodes.or(events); + } + } + /** * Copy all events (if they exist) from the given instance into the current instance. * @@ -417,13 +468,14 @@ */ public void copyEvents(EventDefinition ed) { - if (ed.events == null || ed.events.size() == 0) + if (ed.events == null || ed.events.isEmpty()) { this.event = ed.event; } else { - this.events = new HashSet<>(ed.events); + this.events = new RoaringBitmap(); + this.events.or(ed.events); } } @@ -435,13 +487,14 @@ */ public void copyWidgets(EventDefinition ed) { - if (ed.widgets == null || ed.widgets.size() == 0) + if (ed.widgets == null || ed.widgets.isEmpty()) { this.widget = ed.widget; } else { - this.widgets = new HashSet<>(ed.widgets); + this.widgets = new RoaringBitmap(); + this.widgets.or(ed.widgets); } } @@ -453,13 +506,14 @@ */ public void copyResources(EventDefinition ed) { - if (ed.resources == null || ed.resources.size() == 0) + if (ed.resources == null || ed.resources.isEmpty()) { resource = ed.resource; } else { - this.resources = new HashSet<>(ed.resources); + this.resources = new Roaring64Bitmap(); + this.resources.or(ed.resources); } } @@ -505,16 +559,17 @@ { if (other.event != null) { - return events.size() == 1 && events.contains(other.event); + return events.getCardinality() == 1 && events.contains(other.event); } else { - return events.size() == 0; + return events.isEmpty(); } } else { - return events.size() == other.events.size() && events.containsAll(other.events); + return events.getCardinality() == other.events.getCardinality() && + Utils.containsAll(events, other.events); } } else @@ -541,11 +596,11 @@ { if (event != null) { - return other.events.size() == 1 && other.events.contains(event); + return other.events.getCardinality() == 1 && other.events.contains(event); } else { - return other.events.size() == 0; + return other.events.isEmpty(); } } } @@ -566,9 +621,9 @@ if (widgets != null) { - if (other.widgets == null || - widgets.size() != other.widgets.size() || - !widgets.containsAll(other.widgets)) + if (other.widgets == null || + widgets.getCardinality() != other.widgets.getCardinality() || + !Utils.containsAll(widgets, other.widgets)) return false; } else @@ -595,9 +650,9 @@ if (resources != null) { - if (other.resources == null || - resources.size() != other.resources.size() || - !resources.containsAll(other.resources)) + if (other.resources == null || + resources.getIntCardinality() != other.resources.getIntCardinality() || + !Utils.containsAll(resources, other.resources)) return false; } else @@ -651,17 +706,17 @@ * Check if the given event name is part of this instance. * * @param name - * The event name to test. The case sensitivity matters. + * The event ID to test. * * @return true if the event has been defined for particular event name. */ - public boolean isEventMatch(String name) + public boolean isEventMatch(Integer name) { if (name == null) return false; - return (events != null && events.size() > 0) ? events.contains(name) - : (event != null && event.equals(name)); + return (events != null && !events.isEmpty()) ? events.contains(name) + : event.equals(name); } /** @@ -684,7 +739,7 @@ */ public boolean hasEvent() { - return (events != null && events.size() > 0) || event != null; + return (events != null && !events.isEmpty()) || event != null; } /** @@ -694,7 +749,7 @@ */ public boolean hasWidget() { - return (widgets != null && widgets.size() > 0) || widget != WidgetId.INVALID_WIDGET_ID; + return (widgets != null && !widgets.isEmpty()) || widget != WidgetId.INVALID_WIDGET_ID; } /** @@ -704,39 +759,66 @@ */ public boolean hasResource() { - return (resources != null && resources.size() > 0) || resource != ResourceIdHelper.INVALID_RESOURCE; + return (resources != null && !resources.isEmpty()) || resource != ResourceIdHelper.INVALID_RESOURCE; } /** * Search for registered ROW-DISPLAY events. * - * @return a map containing pairs of (widgetId, triggerId). may be null + * @return an array with elements on index 0 being the list of widget IDs and on index 1 the trigger IDs. */ - public Map getRowDisplayEvents() + public int[][] getRowDisplayEvents() { - Map res = null; + int[][] res = null; // TODO: what about global anywhere? - if (isEventMatch(Keyboard.ROW_DISPLAY) && (anywhere || hasWidget())) + if (isEventMatch(Keyboard.SE_ROW_DISPLAY) && (anywhere || hasWidget())) { - res = new HashMap(); - - if (anywhere) - { - res.put(-1, triggerId); + int length = 0; + + if (anywhere) + { + length++; + } + + if (widgets != null) + { + length += widgets.getCardinality(); + } + else + { + if (widget != WidgetId.INVALID_WIDGET_ID) + { + length++; + } + } + + res = new int[2][length]; + + int idx = 0; + if (anywhere) + { + res[0][idx] = -1; + res[1][idx] = triggerId; + idx = idx + 1; } if (widgets != null) { for (int wid : widgets) { - res.put(wid, triggerId); + res[0][idx] = wid; + res[1][idx] = triggerId; + idx = idx + 1; } } else { if (widget != WidgetId.INVALID_WIDGET_ID) - res.put(widget, triggerId); + { + res[0][idx] = widget; + res[1][idx] = triggerId; + } } } @@ -775,7 +857,7 @@ * @param widgetId * Widget ID to check or -1 if this is a global anywhere event. * @param event - * The event name in the proper case (the caller should take care). + * The event ID. * @param isTrigger * true if the search is for a trigger or false to * seach for an exit condition. @@ -783,76 +865,107 @@ * true if an exact match is to be returned. * @param universal * true if the event is of the universal class. - * - * @return Returns the trigger ID found or -1 for triggers; Returns 0 or -1 for exit - * conditions. In either case, -1 means no match was found. + * @param result + * On output both trigger ID and matched widget ID may be modified. If no match + * was found, then the trigger ID will be set to -1. */ - public int lookup(int frameId, - int widgetId, - String event, - boolean isTrigger, - boolean specific, - boolean universal) + public void lookup(int frameId, + int widgetId, + Integer event, + boolean isTrigger, + boolean specific, + boolean universal, + TriggerMatch result) { + result.matchedWidgetId = -1; + // check the search type if (isTrigger && triggerId == -1) { // no match; not a trigger - return -1; + result.triggerId = -1; + return; } if (!isTrigger && triggerId != -1) { // no match; not an exit condition - return -1; + result.triggerId = -1; + return; } // check if the event is listed if (!isEventMatch(event)) { - return -1; + result.triggerId = -1; + return; } // check global anywhere case if (globalAnywhere && widgetId == -1) { - return isTrigger ? triggerId : 0; + result.triggerId = isTrigger ? triggerId : 0; + return; } if (!hasWidget() && widgetId != -1) { // no match; only global anywhere - return -1; + result.triggerId = -1; + return; } if (widgetId == -1) { // no match; only specific widgets - return -1; + result.triggerId = -1; + return; } // check specific widget if (isWidgetMatch(widgetId)) { - return isTrigger ? triggerId : 0; + result.triggerId = isTrigger ? triggerId : 0; + return; } if (specific && !anywhere) // exact match but if not anywhere { - return -1; + result.triggerId = -1; + return; } // check anywhere within the frame if (!anywhere && !universal) { - return -1; + result.triggerId = -1; + return; + } + + // WARNING: This code can currently only run on the client due to direct references to ThinClient, + // Browse, TitledWindow, Window and Widget. + Widget w = ThinClient.getInstance().registry().getComponent(widgetId); + Browse parentBrowse = w != null ? (Browse) w.parent(Browse.class) : null; + if (parentBrowse != null) + { + // If an event occurs for an in-browse editor, and there is a trigger for the browse column, it + // should be fired. But if there is not column trigger and there is an OF BROWSE br ANYWHERE trigger, + // it should be fired instead. This case is handled here. + int browseId = parentBrowse.getId().asInt(); + if (isWidgetMatch(browseId)) + { + result.triggerId = isTrigger ? triggerId : 0; + result.matchedWidgetId = browseId; + return; + } } if (frameId != -1) { if (isWidgetMatch(frameId)) { - return isTrigger ? triggerId : 0; + result.triggerId = isTrigger ? triggerId : 0; + return; } } else @@ -867,7 +980,8 @@ { if (widget.getId() != null && isWidgetMatch(widget.getId().asInt())) { - return isTrigger ? triggerId : 0; + result.triggerId = isTrigger ? triggerId : 0; + return; } } } @@ -878,13 +992,14 @@ { if (isWidgetMatch(parentFrameId)) { - return isTrigger ? triggerId : 0; + result.triggerId = isTrigger ? triggerId : 0; + return; } } } } - return -1; + result.triggerId = -1; } /** @@ -894,7 +1009,7 @@ * @param resourceId * The ID of the resource. * @param event - * event name in a proper case (the caller should take care) + * event ID. * @param trigExit * flag, telling the search is for a trigger (true) * or for an exit condition (false). @@ -903,7 +1018,7 @@ * returns event index or -1 for exit conditions. In either case, * -1 means no match was found. */ - public int lookup(long resourceId, String event, boolean trigExit) + public int lookup(long resourceId, Integer event, boolean trigExit) { // check the search type if (trigExit && triggerId == -1) @@ -1125,8 +1240,7 @@ } else { - ned.events = new HashSet(); - Utils.intersection(events, ed.events, ned.events); + ned.events = RoaringBitmap.and(events, ed.events); } } else @@ -1173,8 +1287,7 @@ } else { - ned.widgets = new HashSet(); - Utils.intersection(widgets, ed.widgets, ned.widgets); + ned.widgets = RoaringBitmap.and(widgets, ed.widgets); } } else @@ -1221,8 +1334,9 @@ } else { - ned.resources = new HashSet(); - Utils.intersection(resources, ed.resources, ned.resources); + ned.resources = new Roaring64Bitmap(); + ned.resources.or(resources); + ned.resources.and(ed.resources); } } else @@ -1296,22 +1410,22 @@ return; } - if (events != null && events.size() > 0) + if (events != null && !events.isEmpty()) { - if (ed.events != null && ed.events.size() > 0) + if (ed.events != null && !ed.events.isEmpty()) { - ned.events = new HashSet(); - Utils.difference(events, ed.events, ned.events); + ned.events = RoaringBitmap.andNot(events, ed.events); } else { - ned.events = new HashSet(events); + ned.events = new RoaringBitmap(); + ned.events.or(events); ned.events.remove(ed.event); } } else { - if (ed.events != null && ed.events.size() > 0) + if (ed.events != null && !ed.events.isEmpty()) { if (!ed.events.contains(event)) { @@ -1351,22 +1465,22 @@ return; } - if (widgets != null && widgets.size() > 0) + if (widgets != null && !widgets.isEmpty()) { - if (ed.widgets != null && ed.widgets.size() > 0) + if (ed.widgets != null && !ed.widgets.isEmpty()) { - ned.widgets = new HashSet(); - Utils.difference(widgets, ed.widgets, ned.widgets); + ned.widgets = RoaringBitmap.andNot(widgets, ed.widgets); } else { - ned.widgets = new HashSet(widgets); + ned.widgets = new RoaringBitmap(); + ned.widgets.or(widgets); ned.widgets.remove(ed.widget); } } else { - if (ed.widgets != null && ed.widgets.size() > 0) + if (ed.widgets != null && !ed.widgets.isEmpty()) { if (!ed.widgets.contains(widget)) { @@ -1406,22 +1520,24 @@ return; } - if (resources != null && resources.size() > 0) + if (resources != null && !resources.isEmpty()) { - if (ed.resources != null && ed.resources.size() > 0) + if (ed.resources != null && !ed.resources.isEmpty()) { - ned.resources = new HashSet(); - Utils.difference(resources, ed.resources, ned.resources); + ned.resources = new Roaring64Bitmap(); + ned.resources.or(resources); + ned.resources.andNot(ed.resources); } else { - ned.resources = new HashSet(resources); - ned.resources.remove(ed.resources); + ned.resources = new Roaring64Bitmap(); + ned.resources.or(resources); + ned.resources.removeLong(ed.resource); } } else { - if (ed.resources != null && ed.resources.size() > 0) + if (ed.resources != null && !ed.resources.isEmpty()) { if (!ed.resources.contains(resource)) { @@ -1523,12 +1639,9 @@ { if (hasEvent()) { - if (events != null && events.size() > 0) + if (events != null && !events.isEmpty()) { - for (String name : events) - { - generateAtoms(name, atoms); - } + events.forEach((IntConsumer) evt -> generateAtoms(evt, atoms)); } else { @@ -1536,7 +1649,49 @@ } } } - + + /** + * Apply the given function to all widget IDs. + * + * @param f + * The function to apply. + */ + public void processWidgets(Consumer f) + { + if (widgets == null || widgets.isEmpty()) + { + if (widget != WidgetId.INVALID_WIDGET_ID) + { + f.accept(widget); + } + } + else + { + widgets.forEach((IntConsumer) (wid -> f.accept(wid))); + } + } + + /** + * Apply the given function to all resource IDs. + * + * @param f + * The function to apply. + */ + public void processResources(Consumer f) + { + if (resources == null || resources.isEmpty()) + { + if (resource != ResourceIdHelper.INVALID_RESOURCE) + { + f.accept(resource); + } + } + else + { + resources.forEach((LongConsumer) (resid -> f.accept(resid))); + } + } + /** * Replacement for the default object reading method. The latest * state is read from the input source. @@ -1571,11 +1726,12 @@ { if (eventSet) { - events = readStringSet(in, true); + events = new RoaringBitmap(); + events.readExternal(in); } else { - event = readString(in); + event = readInteger(in); } } @@ -1583,7 +1739,8 @@ { if (widgetSet) { - widgets = readIntSet(in); + widgets = new RoaringBitmap(); + widgets.readExternal(in); } else { @@ -1595,7 +1752,8 @@ { if (resourceSet) { - resources = readLongSet(in); + resources = new Roaring64Bitmap(); + resources.readExternal(in); } else { @@ -1618,11 +1776,11 @@ throws IOException { boolean hasEvents = hasEvent(); - boolean eventSet = events != null && events.size() > 0; + boolean eventSet = events != null && !events.isEmpty(); boolean hasWidgets = hasWidget(); - boolean widgetSet = widgets != null && widgets.size() > 0; + boolean widgetSet = widgets != null && !widgets.isEmpty(); boolean hasResources = hasResource(); - boolean resourceSet = resources != null && resources.size() > 0; + boolean resourceSet = resources != null && !resources.isEmpty(); byte state = 0x00; @@ -1642,11 +1800,11 @@ { if (eventSet) { - writeStringSet(out, events); + events.writeExternal(out); } else { - writeString(out, event); + writeInteger(out, event); } } @@ -1654,7 +1812,7 @@ { if (widgetSet) { - writeIntSet(out, widgets); + widgets.writeExternal(out); } else { @@ -1666,7 +1824,7 @@ { if (resourceSet) { - writeLongSet(out, resources); + resources.writeExternal(out); } else { @@ -1685,15 +1843,15 @@ { return String.format("EventDefinition { events %s; widgets %s; resources %s; anywhere %b;" + " globalAnywhere %b; trigger id %d }", - events != null && events.size() > 0 ? events.toString() : "[" + event + "]", - widgets != null && widgets.size() > 0 + events != null && !events.isEmpty() ? toString(events) : "[" + toString(event) + "]", + widgets != null && !widgets.isEmpty() ? widgets.toString() - : (widget != WidgetId.INVALID_WIDGET_ID) + : (widget == WidgetId.INVALID_WIDGET_ID) ? "[]" : "[" + Integer.toString(widget) + "]", - resources != null && resources.size() > 0 + resources != null && !resources.isEmpty() ? resources.toString() - : (resource != ResourceIdHelper.INVALID_RESOURCE) + : (resource == ResourceIdHelper.INVALID_RESOURCE) ? "[]" : "[" + Long.toString(resource) + "]", anywhere, @@ -1707,13 +1865,13 @@ * @param wids * The collection location. */ - void collectWidgetIds(Set wids) + void collectWidgetIds(RoaringBitmap wids) { if (hasWidget()) { - if (widgets != null && widgets.size() > 0) + if (widgets != null && !widgets.isEmpty()) { - wids.addAll(widgets); + wids.or(widgets); } else { @@ -1728,9 +1886,9 @@ * @return the widget id list registered for this event. may be null, in * case of anywhere globally. */ - Set getWidgetIds() + RoaringBitmap getWidgetIds() { - Set wids = new HashSet(); + RoaringBitmap wids = new RoaringBitmap(); collectWidgetIds(wids); @@ -1743,13 +1901,13 @@ * @param res * The collection location. */ - void collectResourceIds(Set res) + void collectResourceIds(Roaring64Bitmap res) { if (hasResource()) { - if (resources != null && resources.size() > 0) + if (resources != null && !resources.isEmpty()) { - res.addAll(resources); + res.or(resources); } else { @@ -1774,11 +1932,13 @@ if (rid != ResourceIdHelper.INVALID_RESOURCE && hasResource()) { - if (resources != null && resources.size() > 0) + if (resources != null && !resources.isEmpty()) { - cleaned = resources.remove(rid); + long sz1 = resources.getLongCardinality(); + resources.removeLong(rid); + cleaned = resources.getLongCardinality() != sz1; - if (resources.size() == 0) + if (resources.isEmpty()) { resources = null; } @@ -1795,11 +1955,11 @@ if (wid != WidgetId.INVALID_WIDGET_ID && hasWidget()) { - if (widgets != null && widgets.size() > 0) + if (widgets != null && !widgets.isEmpty()) { - cleaned = widgets.remove(wid) || cleaned; + cleaned = widgets.checkedRemove(wid) || cleaned; - if (widgets.size() == 0) + if (widgets.isEmpty()) { widgets = null; } @@ -1822,15 +1982,15 @@ * for global-anywhere (if that is set). Insert all new atoms into the given list. * * @param name - * The event name. + * The event ID. * @param atoms * The list to collect the results. */ - private void generateAtoms(String name, List atoms) + private void generateAtoms(Integer name, List atoms) { if (hasWidget()) { - if (widgets != null && widgets.size() > 0) + if (widgets != null && !widgets.isEmpty()) { for (Integer wid: widgets) { @@ -1845,12 +2005,9 @@ if (hasResource()) { - if (resources != null && resources.size() > 0) + if (resources != null && !resources.isEmpty()) { - for (Long resid: resources) - { - atoms.add(new EventDefinition(anywhere, resid, name)); - } + resources.forEach(resid -> atoms.add(new EventDefinition(anywhere, resid, name))); } else { @@ -1863,4 +2020,37 @@ atoms.add(new EventDefinition(name)); } } + + /** + * Get the string representation of this event. + * + * @param event + * The event ID. + */ + private String toString(Integer event) + { + return event == null ? "n/a" : Keyboard.eventName(event); + } + + /** + * Get the string representation of all the events in this bitmap. + * + * @param events + * The event IDs. + */ + private String toString(RoaringBitmap events) + { + StringBuilder sb = new StringBuilder(); + sb.append("["); + events.forEach((IntConsumer) (evt -> + { + if (sb.length() > 1) + { + sb.append(", "); + } + sb.append(Keyboard.eventName(evt)); + })); + sb.append("]"); + return sb.toString(); + } } === modified file 'src/com/goldencode/p2j/ui/EventList.java' --- src/com/goldencode/p2j/ui/EventList.java 2020-10-08 20:57:05 +0000 +++ src/com/goldencode/p2j/ui/EventList.java 2021-01-22 15:19:01 +0000 @@ -3,7 +3,7 @@ ** Abstract : encodes events and widgets that determine the condition of ** termination of the Progress WAIT-FOR statement ** -** Copyright (c) 2007-2020, Golden Code Development Corporation. +** Copyright (c) 2007-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------- ** 001 NVS 20070115 @31883 Created based on the WaitList.java file. @@ -137,6 +137,14 @@ ** CA 20201003 Pass the LogicalTerminal instance to cleanup(), to avoid context-local lookups. ** GES 20201004 Fixed regression in anywhere flag by change in addEvent(). ** 053 IAS 20201008 Optimized iteration over LinkedList in the lookupEventHelper. +** CA 20201010 Use a fast-access bitmap instead of set for widget and resource IDs. +** CA 20201015 Avoid LinkedList 'events' for the instance returned by difference, intersection +** APIs, to allow fast iteration. +** SVL 20201219 Reflected changes in EventDefinition. +** CA 20210118 Performance improvement: do not transfer same EventList instance more than once +** to the client-side. The client is responsible of using the last received +** instance, instead of reading it from the socket. +** CA 20210120 Events prefixed with SHIFT-CTRL- must set the prefix as CTRL-SHIFT-. */ /* @@ -197,8 +205,12 @@ import static com.goldencode.util.NativeTypeSerializer.*; import java.io.*; import java.util.*; +import java.util.function.Consumer; import java.util.stream.*; +import org.roaringbitmap.*; +import org.roaringbitmap.longlong.*; + import com.goldencode.p2j.ui.chui.ThinClient; import com.goldencode.p2j.ui.client.*; import com.goldencode.p2j.ui.client.event.*; @@ -240,6 +252,13 @@ /** The highest code */ static final int CODE_MAX = CHOOSE; + /** + * Last-read cache of the EventList instance - used only when the server-side informs the client-side that + * the cache should be used (same instance is re-sent to the client). Used only by the client's Reader + * thread. + */ + private static final ThreadLocal lastReadInstance = new ThreadLocal<>(); + /** Type of operation */ private int operation = WAIT_FOR; @@ -247,7 +266,7 @@ private Set goOn = null; /** Set of validatable widgets, if present. */ - private Set validations = null; + private RoaringBitmap validations = null; /** List of event definitions */ private List events = new ArrayList<>(); @@ -258,6 +277,18 @@ /** Color values' (de)serializer */ private static Externalizer EXT = new Externalizer<>(EventList.class, GoOn.class); + /** A cache of {@link #lookup(int, int, boolean, boolean, boolean, TriggerMatch)} results. */ + private Map lookupWidgets = null; + + /** A cache of {@link #lookup(long, boolean, TriggerMatch)} results. */ + private Map lookupResources = null; + + /** A bitmap of all event codes in all {@link #events}. */ + private RoaringBitmap eventCodes = null; + + /** Flag indicating if this instance was previously sent to the client-side. Used only on server-side. */ + private boolean cached = false; + /** * Default constructor with no events. Call addEvent() to configure this * instance. This would be used for definitions that have multiple mappings of events @@ -685,6 +716,41 @@ } /** + * Calculate the event or key code for this event name. + * + * @param name + * The event name. + */ + static Integer eventCode(String name) + { + int evtCode = Keyboard.eventCode(name); + int keyCode = Keyboard.keyCode(name); + if (keyCode != -1) + { + String primary = Keyboard.keyLabel(keyCode); + if (!primary.equals(name) && primary.length() > 0) + { + name = primary; + keyCode = Keyboard.keyCode(name); + evtCode = Keyboard.eventCode(name); + } + } + + if (keyCode == -1 && evtCode == -1) + { + return null; + } + else if (keyCode != -1) + { + return keyCode; + } + else + { + return evtCode; + } + } + + /** * Calculate the differences between two event lists. An array of two event lists is returned. * The first represents the event definitions present in {@code from}, but not in {@code to} * (i.e., the list of dead events). The second represents the event definitions present in @@ -1183,20 +1249,20 @@ } } - events.add(new EventDefinition(name, id, hid, anywhere)); + addEvent(new EventDefinition(eventCode(name), id, hid, anywhere)); return; } // create a set of events - Set evts = null; + RoaringBitmap evts = null; if (event.length > 1) { - evts = new HashSet<>(); + evts = new RoaringBitmap(); for (int i = 0; i < event.length; i ++) { - evts.add(preprocessEventName(event[i])); + evts.add(eventCode(preprocessEventName(event[i]))); } } @@ -1218,13 +1284,13 @@ } } - events.add(new EventDefinition(evts, id, hid, anywhere)); + addEvent(new EventDefinition(evts, id, hid, anywhere)); return; } // create a set of IDs - Set ids = new HashSet<>(); - Set hids = new HashSet<>(); + RoaringBitmap ids = new RoaringBitmap(); + Roaring64Bitmap hids = new Roaring64Bitmap(); if (target != null) { @@ -1252,12 +1318,12 @@ { String name = preprocessEventName(event[0]); - events.add(new EventDefinition(name, ids, hids, anywhere)); + addEvent(new EventDefinition(eventCode(name), ids, hids, anywhere)); return; } // add new event definition to the list (slow/Set version for everything!) - events.add(new EventDefinition(evts, ids, hids, anywhere)); + addEvent(new EventDefinition(evts, ids, hids, anywhere)); } /** @@ -1317,25 +1383,41 @@ /** * Search for registered ROW-DISPLAY events. * - * @return a map containing pairs of (widgetId, triggerId). May be null. + * @return an array with elements on index 0 being the list of widget IDs and on index 1 the trigger IDs. */ - public Map getRowDisplayEvents() + public int[][] getRowDisplayEvents() { - Map res = null; + List lres = null; + int length = 0; for (EventDefinition ed : events) { - Map m = ed.getRowDisplayEvents(); - if (m != null) + int[][] arr = ed.getRowDisplayEvents(); + if (arr != null) { - if (res == null) + if (lres == null) { - res = new HashMap<>(); + lres = new ArrayList<>(); } - res.putAll(m); + lres.add(arr); + length += arr[0].length; } } + if (lres == null || lres.isEmpty()) + { + return null; + } + + int[][] res = new int[2][length]; + int idx = 0; + for (int[][] arr : lres) + { + System.arraycopy(arr[0], 0, res[0], idx, arr[0].length); + System.arraycopy(arr[1], 0, res[1], idx, arr[1].length); + idx += arr[0].length; + } + return res; } @@ -1350,7 +1432,7 @@ */ public int[] getExitWidgetIds() { - Set res = new HashSet(); + RoaringBitmap res = new RoaringBitmap(); for (EventDefinition ed : events) { @@ -1361,7 +1443,7 @@ } } - return (res.size() == 0) ? null : Utils.integerCollectionToPrimitive(res); + return res.isEmpty() ? null : res.toArray(); } /** @@ -1374,7 +1456,7 @@ */ public long[] getExitResourceIds() { - Set res = new HashSet(); + Roaring64Bitmap res = new Roaring64Bitmap(); for (EventDefinition ed : events) { @@ -1385,7 +1467,7 @@ } } - return (res.size() == 0) ? null : Utils.longCollectionToPrimitive(res); + return res.isEmpty() ? null : res.toArray(); } /** @@ -1495,7 +1577,7 @@ if (validations == null) { - validations = new HashSet<>(); + validations = new RoaringBitmap(); } validations.add(widget.getId()); @@ -1603,6 +1685,9 @@ // save the events this.events = keep.events == null ? new ArrayList<>() : keep.events; + + // invalidate the lookup result cache + invalidateCache(); } /** @@ -1680,6 +1765,8 @@ goOn.addAll(from.goOn); } + invalidateCache(); + return this; } @@ -1825,22 +1912,40 @@ */ public void lookup(long resourceId, boolean trigExit, TriggerMatch result) { - String event = Keyboard.eventName(result.eventId); - - for (EventDefinition ed : events) - { - // always searching for exit conditions - result.triggerId = ed.lookup(resourceId, event, trigExit); - - if (result.triggerId != -1) - { - break; - } - } - - if (result.triggerId == -1 && chain != null) - { - chain.lookup(resourceId, trigExit, result); + LookupResourceKey key = new LookupResourceKey(resourceId, trigExit); + if (lookupResources == null) + { + lookupResources = new HashMap<>(); + } + TriggerMatch res = lookupResources.get(key); + if (res != null) + { + result.triggerId = res.triggerId; + result.matchedWidgetId = res.matchedWidgetId; + return; + } + + try + { + for (EventDefinition ed : events) + { + // always searching for exit conditions + result.triggerId = ed.lookup(resourceId, result.eventId, trigExit); + + if (result.triggerId != -1) + { + break; + } + } + + if (result.triggerId == -1 && chain != null) + { + chain.lookup(resourceId, trigExit, result); + } + } + finally + { + lookupResources.put(key, new TriggerMatch(result)); } } @@ -1948,7 +2053,7 @@ nel.operation = this.operation; nel.goOn = this.goOn; nel.validations = this.validations; - nel.events = l; + nel.events = new ArrayList<>(l); return nel; } @@ -2005,7 +2110,7 @@ nel.operation = this.operation; nel.goOn = this.goOn; nel.validations = this.validations; - nel.events = from; + nel.events = new ArrayList<>(from); return nel; } @@ -2044,7 +2149,7 @@ nel.operation = this.operation; nel.goOn = this.goOn; nel.validations = this.validations; - nel.events = l; + nel.events = new ArrayList<>(l); return nel; } @@ -2089,12 +2194,37 @@ throws IOException, ClassNotFoundException { + boolean useCache = in.readBoolean(); + + if (useCache) + { + // set all fields from cache + EventList el = lastReadInstance.get(); + + operation = el.operation; + goOn = el.goOn; + validations = el.validations; + events = el.events; + chain = null; // never serialized + lookupWidgets = el.lookupWidgets; + lookupResources = el.lookupResources; + cached = true; + + return; + } + operation = in.readInt(); - goOn = readStringSet(in, false); - validations = readIntSet(in); + goOn = readStringSet(in, false); + if (in.readBoolean()) + { + validations = new RoaringBitmap(); + validations.readExternal(in); + } events.addAll(readList(in, EventDefinition::new, true)); // chaining is not transferred + + lastReadInstance.set(this); } /** @@ -2143,12 +2273,26 @@ public void writeExternal(ObjectOutput out) throws IOException { + // we avoid transferring to the client-side the same event list + out.writeBoolean(cached); + if (cached) + { + return; + } + out.writeInt(operation); writeStringSet(out, goOn); - writeIntSet(out, validations); + out.writeBoolean(validations != null); + if (validations != null) + { + validations.writeExternal(out); + } writeList(out, events); // chaining is not transferred + + // mark this instance as 'cached' so it will not be transferred again. + cached = true; } /** @@ -2211,6 +2355,28 @@ } /** + * Apply the given function to all {@link #events}. + * + * @param f + * The function to apply. + */ + public void processDefinitions(Consumer f) + { + if (events != null) + { + for (EventDefinition ed : events) + { + f.accept(ed); + } + } + + if (chain != null) + { + chain.processDefinitions(f); + } + } + + /** * Renders the instances as text. * * @return The text representation of this instance. @@ -2277,16 +2443,29 @@ * original event name * @return event name suitable for storing */ - private String setCase(String event) + static String setCase(String event) { - int kc = Keyboard.keyCode(event); + Integer kc = eventCode(event); - if (kc == -1 || event.length() > 1) + if (kc == null || kc == -1 || event.length() > 1) { event = event.toUpperCase(); + if (event.startsWith("CTRL-ALT-")) { - event = "ALT-CTRL-" + event.substring("CTRL-ALT-".length()); + String other = "ALT-CTRL-" + event.substring("CTRL-ALT-".length()); + if (eventCode(other) != null) + { + event = other; + } + } + else if (event.startsWith("SHIFT-CTRL-")) + { + String other = "CTRL-SHIFT-" + event.substring("SHIFT-CTRL-".length()); + if (eventCode(other) != null) + { + event = other; + } } // translate to upper case @@ -2323,136 +2502,152 @@ private void lookupWorker(int fid, int wid, boolean trig, boolean isKey, boolean specificWidget, TriggerMatch result) { - // obtain the event name and class - String event = Keyboard.eventName(result.eventId); - boolean universal = EventDefinition.isEventUniversal(result.eventId); - - // convert the event name to the proper case - String ev = setCase(event); - // convert the label to a key code, alternate labels are honored here - int code = Keyboard.keyCode(ev); - - // is this a valid label? - if (code != -1) - { - // yes, it was a label so now convert the common code to the primary label name - String primary = Keyboard.keyLabel(code); - ev = (primary.length() > 0) ? primary : ev; - } - - // lookup event for a specific widget and honor any-printable/any-key if needed - lookupEventHelper(-1, wid, ev, trig, true, false, isKey, result); - - if (result.triggerId != -1) - return; - - if (specificWidget) - { - return; - } - - // lookup event for a anywhere in the frame and honor any-printable/any-key if needed - lookupEventHelper(fid, wid, ev, trig, false, universal, isKey, result); - - if (result.triggerId != -1) - { - result.matchedWidgetId = wid; - return; - } - - // look the list up for the specified event and the containing frame - if ((trig && universal) || !trig) - { - for (EventDefinition ed : events) - { - result.triggerId = ed.lookup(-1, fid, ev, trig, true, false); - - if (result.triggerId != -1) - { - result.matchedWidgetId = fid; - return; - } - } - } - - Widget widget = ThinClient.getInstance().getWidget(wid); - - if (widget != null) - { - List> parentFrames = UiUtils.getParentFrames(widget); - - // search the target trigger through parent frames - for (Frame frame : parentFrames) - { - int id = frame.getId().asInt(); - - if (id != wid && id != fid && frame.isVisible() && !frame.hidden()) - { - lookupEventHelper(id, wid, ev, trig, true, false, isKey, result); - } - - if (result.triggerId != -1) - { - result.matchedWidgetId = wid; - return; - } - } - - TopLevelWindow win = null; - Frame rootFrame = UiUtils.locateRootFrame(widget); - if (rootFrame != null) - { - win = rootFrame.topLevelWindow(); - } - else if (widget instanceof TopLevelWindow) - { - win = (TopLevelWindow) widget; - } - - // search the target trigger in its top level window - if (win != null && - win.getId() != WidgetId.DEFAULT_WINDOW_ID && - win.isVisible() && !win.hidden()) - { - int winid = win.getId().asInt(); - // lookup event for a specific widget and honor any-printable/any-key if needed - lookupEventHelper(rootFrame == null ? -1 : rootFrame.getId().asInt(), - winid, ev, trig, true, false, isKey, result); - - if (result.triggerId != -1) - { - result.matchedWidgetId = winid; - } - } - } - - if (result.triggerId != -1) - return; - - // lookup event for a anywhere globally and honor any-printable/any-key if needed - lookupEventHelper(-1, -1, ev, trig, false, false, isKey, result); - - if (result.triggerId != -1) - return; - - // look the list up for the specified event and the session window - if (!trig) - { - for (EventDefinition ed : events) - { - result.triggerId = ed.lookup(-1, - WidgetId.DEFAULT_WINDOW_ID.asInt(), - ev, - trig, - true, - false); - - if (result.triggerId != -1) - { - result.matchedWidgetId = WidgetId.DEFAULT_WINDOW_ID.asInt(); - return; - } - } + LookupWidgetKey key = new LookupWidgetKey(fid, wid, trig, isKey, specificWidget, result.eventId); + if (lookupWidgets == null) + { + lookupWidgets = new HashMap<>(); + } + TriggerMatch res = lookupWidgets.get(key); + if (res != null) + { + result.triggerId = res.triggerId; + result.matchedWidgetId = res.matchedWidgetId; + return; + } + + try + { + // obtain the event name and class + String event = Keyboard.eventName(result.eventId); + boolean universal = EventDefinition.isEventUniversal(result.eventId); + + // convert the event name to the proper case + String ev = setCase(event); + // convert the label to a key code, alternate labels are honored here + int code = Keyboard.keyCode(ev); + + // is this a valid label? + if (code != -1) + { + // yes, it was a label so now convert the common code to the primary label name + String primary = Keyboard.keyLabel(code); + ev = (primary.length() > 0) ? primary : ev; + } + Integer evCode = eventCode(ev); + + // lookup event for a specific widget and honor any-printable/any-key if needed + lookupEventHelper(-1, wid, evCode, trig, true, false, isKey, result); + + if (result.triggerId != -1) + { + return; + } + + if (specificWidget) + { + return; + } + + // lookup event for a anywhere in the frame and honor any-printable/any-key if needed + lookupEventHelper(fid, wid, evCode, trig, false, universal, isKey, result); + + if (result.triggerId != -1) + { + result.matchedWidgetId = wid; + return; + } + + // look the list up for the specified event and the containing frame + if (!trig || universal) + { + for (EventDefinition ed : events) + { + ed.lookup(-1, fid, evCode, trig, true, false, result); + if (result.triggerId != -1) + { + result.matchedWidgetId = fid; + return; + } + } + } + + Widget widget = ThinClient.getInstance().getWidget(wid); + + if (widget != null) + { + List> parentFrames = UiUtils.getParentFrames(widget); + + // search the target trigger through parent frames + for (Frame frame : parentFrames) + { + int id = frame.getId().asInt(); + + if (id != wid && id != fid && frame.isVisible() && !frame.hidden()) + { + lookupEventHelper(id, wid, evCode, trig, true, false, isKey, result); + } + + if (result.triggerId != -1) + { + result.matchedWidgetId = wid; + return; + } + } + + TopLevelWindow win = null; + Frame rootFrame = UiUtils.locateRootFrame(widget); + if (rootFrame != null) + { + win = rootFrame.topLevelWindow(); + } + else if (widget instanceof TopLevelWindow) + { + win = (TopLevelWindow) widget; + } + + // search the target trigger in its top level window + if (win != null && + win.getId() != WidgetId.DEFAULT_WINDOW_ID && + win.isVisible() && !win.hidden()) + { + int winid = win.getId().asInt(); + // lookup event for a specific widget and honor any-printable/any-key if needed + lookupEventHelper(rootFrame == null ? -1 : rootFrame.getId().asInt(), + winid, evCode, trig, true, false, isKey, result); + + if (result.triggerId != -1) + { + result.matchedWidgetId = winid; + } + } + } + + if (result.triggerId != -1) + return; + + // lookup event for a anywhere globally and honor any-printable/any-key if needed + lookupEventHelper(-1, -1, evCode, trig, false, false, isKey, result); + + if (result.triggerId != -1) + return; + + // look the list up for the specified event and the session window + if (!trig) + { + for (EventDefinition ed : events) + { + ed.lookup(-1, WidgetId.DEFAULT_WINDOW_ID.asInt(), evCode, trig, true, false, result); + if (result.triggerId != -1) + { + result.matchedWidgetId = WidgetId.DEFAULT_WINDOW_ID.asInt(); + return; + } + } + } + } + finally + { + lookupWidgets.put(key, new TriggerMatch(result)); } } @@ -2471,15 +2666,19 @@ // alternate key labels won't be matched if we don't convert them to the primary label // convert the label to a key code, alternate labels are honored here - int code = Keyboard.keyCode(caseEvent); + Integer code = eventCode(caseEvent); // is this a valid label? - if (code != -1) + if (code != null) { // yes, it was a label so now convert the common code to the primary label name String primary = Keyboard.keyLabel(code); caseEvent = (primary.length() > 0) ? primary : caseEvent; } + else + { + throw new IllegalStateException("Event [" + name + "] is not registered in FWD!"); + } return caseEvent; } @@ -2572,21 +2771,34 @@ */ private void lookupEventHelper(int fid, int wid, - String ev, + Integer evCode, boolean trigger, boolean specific, boolean universal, boolean isKey, TriggerMatch result) { + if (eventCodes == null) + { + eventCodes = new RoaringBitmap(); + events.forEach(evt -> evt.gatherEventCodes(eventCodes)); + } + + if (!eventCodes.contains(evCode) && + !eventCodes.contains(Keyboard.SE_ANY_KEY) && + !eventCodes.contains(Keyboard.SE_ANY_PRINTABLE)) + { + return; + } + // look the list up for the specified event and widget save the last found anywhere // trigger id int anywhereTriggerId = -1; + int anywhereTriggerWidget = -1; for (EventDefinition ed: events) { - int triggerId = ed.lookup(fid, wid, ev, trigger, specific, universal); - - if (triggerId != -1) + ed.lookup(fid, wid, evCode, trigger, specific, universal, result); + if (result.triggerId != -1) { if (specific && ed.isAnywhere()) { @@ -2594,12 +2806,12 @@ // replace previously found trigger, exact widget has a priority over parent frame if (anywhereTriggerId == -1 || ed.isWidgetMatch(wid)) { - anywhereTriggerId = triggerId; + anywhereTriggerId = result.triggerId; + anywhereTriggerWidget = result.matchedWidgetId; } } else { - result.triggerId = triggerId; return; } } @@ -2608,21 +2820,16 @@ if (anywhereTriggerId != -1) { result.triggerId = anywhereTriggerId; + result.matchedWidgetId = anywhereTriggerWidget; return; } // honor ANY-PRINTABLE event (must be before ANY-KEY) - if (Keyboard.isPrintable(ev)) + if (Keyboard.isPrintable(Keyboard.eventName(evCode))) { for (EventDefinition ed : events) { - result.triggerId = ed.lookup(fid, - wid, - Keyboard.ANY_PRINTABLE, - trigger, - specific, - universal); - + ed.lookup(fid, wid, Keyboard.SE_ANY_PRINTABLE, trigger, specific, universal, result); if (result.triggerId != -1) { // prevent recursion later by noting which event was really matched @@ -2637,13 +2844,7 @@ { for (EventDefinition ed : events) { - result.triggerId = ed.lookup(fid, - wid, - Keyboard.ANY_KEY, - trigger, - specific, - universal); - + ed.lookup(fid, wid, Keyboard.SE_ANY_KEY, trigger, specific, universal, result); if (result.triggerId != -1) { // prevent recursion later by noting which event was really matched @@ -2655,6 +2856,17 @@ } /** + * Invalidate the cached state. + */ + private void invalidateCache() + { + // invalidate the lookup result cache + this.lookupResources = null; + this.lookupWidgets = null; + this.eventCodes = null; + } + + /** * Stores a widget and resource ID pair. */ static class IdPair @@ -2665,4 +2877,201 @@ /** Resource ID. */ long hid = ResourceIdHelper.INVALID_RESOURCE; } + + /** + * A key for caching the result of {@link EventList#lookup(long, boolean, TriggerMatch)}. + */ + static class LookupResourceKey + { + /** The resource ID. */ + private final long resourceId; + + /** + * true if the search is for a trigger or false if the search is for an exit + * condition. + */ + private final boolean trigExit; + + /** The cached hash value. */ + private final int hash; + + /** + * Create a new instance. + * + * @param resourceId + * The resource ID. + * @param trigExit + * true if the search is for a trigger or false if the search is + * for an exit condition. + */ + public LookupResourceKey(long resourceId, boolean trigExit) + { + this.resourceId = resourceId; + this.trigExit = trigExit; + + this.hash = hash(); + } + + /** + * The key's hash code. + * + * @return The {@link #hash} + */ + @Override + public int hashCode() + { + return hash; + } + + /** + * Check if this instance is the same as the other. + * + * @param obj + * The other instance. + * + * @return true if the instances have the same state. + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof LookupResourceKey)) + { + return false; + } + + LookupResourceKey key = (LookupResourceKey) obj; + + return resourceId == key.resourceId && trigExit == key.trigExit; + } + + /** + * Precalculate the key's hash. + * + * @return See above. + */ + private int hash() + { + int res = 17; + res = res * 37 + Long.hashCode(resourceId); + res = res * 37 + Boolean.hashCode(trigExit); + return res; + } + } + + /** + * A key for caching the result of {@link EventList#lookup(int, int, boolean, boolean, boolean, TriggerMatch)}. + */ + static class LookupWidgetKey + { + /** Frame ID, can be -1 */ + private final int fid; + + /** widget ID, can't be -1, but can be SESSION_WINDOW_ID. */ + private final int wid; + + /** + * true if the search is for a trigger or false to search for an exit + * condition. + */ + private final boolean trig; + + /** Marks real key pressed by user. */ + private final boolean isKey; + + /** + * When true only the specific widget events are lsearched, i.e. anywhere, session window, + * etc. are ignored. + */ + private final boolean specificWidget; + + /** The event code matched in the lookup. */ + private final int eventId; + + /** The cached hash value. */ + private final int hash; + + /** + * Create a new instance. + * + * @param fid + * Frame ID, can be -1 + * @param wid + * widget ID, can't be -1, but can be SESSION_WINDOW_ID. + * @param trig + * true if the search is for a trigger or false to search for an + * exit condition. + * @param isKey + * Marks real key pressed by user. + * @param specificWidget + * When true only the specific widget events are lsearched, i.e. anywhere, + * session window, etc. are ignored. + * @param eventId + * The event code matched in the lookup. + */ + public LookupWidgetKey(int fid, int wid, boolean trig, boolean isKey, boolean specificWidget, int eventId) + { + this.fid = fid; + this.wid = wid; + this.trig = trig; + this.isKey = isKey; + this.specificWidget = specificWidget; + this.eventId = eventId; + + this.hash = hash(); + } + + /** + * The key's hash code. + * + * @return The {@link #hash} + */ + @Override + public int hashCode() + { + return hash; + } + + /** + * Check if this instance is the same as the other. + * + * @param obj + * The other instance. + * + * @return true if the instances have the same state. + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof LookupWidgetKey)) + { + return false; + } + + LookupWidgetKey key = (LookupWidgetKey) obj; + + return fid == key.fid && + wid == key.wid && + trig == key.trig && + isKey == key.isKey && + specificWidget == key.specificWidget && + eventId == key.eventId; + } + + /** + * Precalculate the key's hash. + * + * @return See above. + */ + private int hash() + { + int res = 17; + res = res * 37 + Integer.hashCode(fid); + res = res * 37 + Integer.hashCode(wid); + res = res * 37 + Boolean.hashCode(trig); + res = res * 37 + Boolean.hashCode(isKey); + res = res * 37 + Boolean.hashCode(specificWidget); + res = res * 37 + Integer.hashCode(eventId); + return res; + } + } } === modified file 'src/com/goldencode/p2j/ui/FieldGroup.java' --- src/com/goldencode/p2j/ui/FieldGroup.java 2020-09-27 15:48:49 +0000 +++ src/com/goldencode/p2j/ui/FieldGroup.java 2021-01-27 17:52:59 +0000 @@ -2,7 +2,7 @@ ** Module : FieldGroup.java ** Abstract : groups widgets in the frame on the server side ** -** Copyright (c) 2008-2020, Golden Code Development Corporation. +** Copyright (c) 2008-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 GES 20080318 @37589 Created initial version to group widgets in @@ -60,6 +60,12 @@ ** SVL 20190317 Added cleanNonTabItems. ** 040 HC 20200301 Made FIELD-GROUP:FONT non-queriable (CAN-QUERY must return false). ** 041 SBI 20200729 Fixed index out of bounds exception if widget is added before/after itself. +** 042 CA 20201117 Fixed a memory leak - FIELD-GROUP widgets were not being deleted. +** 043 VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. +** FONT attribute is marked and implemented ad not-queriable and +** non-settable. +** CA 20210127 Fixed a problem in DOWN frames - the literal field-group gets reconstructed, and +** it must be forced to be deleted. */ /* ** This program is free software: you can redistribute it and/or modify @@ -156,6 +162,9 @@ /** Tab item list */ private List> tabItems; + /** Flag indicating that the deletion is forced. */ + private boolean force = true; + /** * Creates an instance to groups related widgets in a frame. * @@ -251,15 +260,41 @@ /** * {@inheritDoc} */ - @LegacyAttribute(name = "FONT", canQuery = false) + @LegacyAttribute(name = "FONT", ignore = true) @Override public integer getFont() { - // this getter is here to provide LegacyAttribute with canQuery set to false - return super.getFont(); + notQueryable("FONT"); + return new integer(); } /** + * Set the FONT attribute of this widget. + * + * @param fontNum + * An entry in the font-table or unknown to refer the default font. + */ + @LegacyAttribute(name = "FONT", setter = true, ignore = true) + @Override + public void setFont(long fontNum) + { + notSettable("FONT"); + } + + /** + * Set the FONT attribute of this widget. + * + * @param fontNum + * An entry in the font-table or unknown to refer the default font. + */ + @LegacyAttribute(name = "FONT", setter = true, ignore = true) + @Override + public void setFont(int64 fontNum) + { + notSettable("FONT"); + } + + /** * Remove the specified widget from this field-group. * * @param widget @@ -276,6 +311,8 @@ this.tabItems.remove(widget); this.firstTabItem = null; this.lastTabItem = null; + + frame.removeWidget(widget); } /** @@ -854,6 +891,28 @@ } /** + * Delete the resource. + *

+ * Field groups need to be manually removed from the registry, when they are deleted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + // reset the frame before calling super, so cannDelete will allowthe deletion. + GenericFrame fgFrame = frame; + frame = null; + super.delete(); + frame = fgFrame; + + LogicalTerminal.deregisterWidget(getAttr("id", () -> config.id).asInt()); + } + + /** * Moves the target widget to the top of this list. * * @param widget @@ -957,23 +1016,34 @@ } /** + * Check if this resource can be deleted (implicitly or not). + * + * @return See above. + */ + @Override + protected boolean canDelete() + { + return force || frame == null || super.canDelete(); + } + + /** * Delete the resource. - *

- * Field groups need to be manually removed from the registry, when they are deleted. * * @return true if the resource was deleted. */ @Override protected boolean resourceDelete() { - boolean delete = super.resourceDelete(); - - if (delete) - { - LogicalTerminal.deregisterWidget(getAttr("id", () -> config.id).asInt()); - } - - return delete; + return force || super.resourceDelete(); + } + + /** + * Force the delete for this field-group. + */ + void forceDelete() + { + this.force = true; + this.delete(); } /** === modified file 'src/com/goldencode/p2j/ui/FillInWidget.java' --- src/com/goldencode/p2j/ui/FillInWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/FillInWidget.java 2021-01-15 20:55:46 +0000 @@ -2,7 +2,7 @@ ** Module : FillInWidget.java ** Abstract : server side FillIn widget implementation ** -** Copyright (c) 2005-2019, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 NVS 20051026 @23490 Created initial version. @@ -52,6 +52,10 @@ ** Adding support for EDIT-CAN-UNDO, EDIT-CAN-PASTE attributes. ** 030 HC 20191120 Negative end range for SET-SELECTION must select the whole screen ** value for COMBO-BOX and FILL-IN widgets. +** 031 EVL 20201022 Optimized attributes flush implementation. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. */ /* ** This program is free software: you can redistribute it and/or modify @@ -235,6 +239,7 @@ * @param newReadOnly * Is widget read only. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) public void setReadOnly(boolean newReadOnly) { if (config.readOnly == newReadOnly) @@ -415,7 +420,7 @@ return new character(""); } - int widthChars = (int) getAttr("widthChars", () -> config.widthChars); + int widthChars = (int) getAttr("widthChars", () -> config.widthChars, true); int selectionStart = getAttr("selectionStart", () -> config.selectionStart); int selectionEnd = getAttr("selectionEnd", () -> config.selectionEnd); @@ -835,7 +840,8 @@ if (isFocused()) { // cursor can take position after the last character of the fill-in - int offset = Math.min(getAttr("cursorOffset", () -> config.cursorOffset) + 1, (int) getAttr("widthChars", () -> config.widthChars)); + int offset = Math.min(getAttr("cursorOffset", () -> config.cursorOffset, true) + 1, + (int) getAttr("widthChars", () -> config.widthChars, true)); return new integer(offset); } else @@ -888,7 +894,7 @@ } final int val = offset.intValue(); - if (val < 1 || val > getAttr("widthChars", () -> config.widthChars)) + if (val < 1 || val > getAttr("widthChars", () -> config.widthChars, true)) { ErrorManager.recordOrShowError(4057, String.format("**Attribute CURSOR-OFFSET for the FILL-IN %s has an invalid " + @@ -950,7 +956,7 @@ * the value as uninitialized. */ @Override - public void setScreenValue(character value) + protected void setScreenValueInt(character value) { // check if we really need to change anything (input variable chack for null) // the new screen value can be the same as the current one and this is not a cause @@ -966,7 +972,7 @@ // of the fill-in can become completely inconsistent in some conditions // the MODIFIED attribute flag is supposed to change to TRUE too (as it does in the 4GL) config.modified = true; - super.setScreenValue(value); + super.setScreenValueInt(value); } /** === modified file 'src/com/goldencode/p2j/ui/FontTable.java' --- src/com/goldencode/p2j/ui/FontTable.java 2020-01-22 09:15:36 +0000 +++ src/com/goldencode/p2j/ui/FontTable.java 2020-12-05 02:01:57 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2014-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- --------------------------------Description----------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 CA 20140701 Created initial version. ** 002 CA 20140711 Added legacy metrics support and font details for legacy font-tables. ** 003 MAG 20140827 Move common code to BaseSysTable class. @@ -55,6 +55,7 @@ ** CPU cycles for a non-critical message. ** 030 HC 20190423 Implemented direct font control, AKA custom font support. ** 031 CA 20200122 Javadoc fixes. +** 032 OM 20201203 Fixed handling of READ/ONLY attributes. */ /* @@ -343,8 +344,7 @@ } /** - * API needed to implement read-only attribute assignment (a 4GL - * "feature"). + * API needed to implement read-only attribute assignment (a 4GL "feature"). * * @param attribute * The attribute's name. @@ -357,6 +357,21 @@ } /** + * API needed to implement read-only attribute assignment (a 4GL "feature"). + * + * @param attribute + * The attribute's name. + * @param expr + * The value which is attempted to be assigned to the read-only attribute. + * + * @see handle#readOnlyError(handle, String, Object) + */ + public static void readOnlyError(String attribute, Object expr) + { + handle.readOnlyError(asHandle(), attribute); + } + + /** * Get the type of its associated handle. * * @return Always return the PSEUDO-WIDGET value. === modified file 'src/com/goldencode/p2j/ui/FrameWidget.java' --- src/com/goldencode/p2j/ui/FrameWidget.java 2020-10-04 18:23:10 +0000 +++ src/com/goldencode/p2j/ui/FrameWidget.java 2020-10-31 00:42:17 +0000 @@ -2,9 +2,9 @@ ** Module : FrameWidget.java ** Abstract : server side frame widget class (contains the configuration) ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23491 Created initial version. ** 002 NVS 20060114 @24097 Frame is created hidden. ** 003 SIY 20060213 @24581 Fixes for the DialogBox support. @@ -145,6 +145,13 @@ ** EVL 20200828 Fixed frame name issue when the name is redefining after frame has been created. ** HC 20201004 Performance optimizations of server-client config state ** synchronization. +** HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201112 Avoid pushing the entire frame definition when setting the FRAME attribute. +** CA 20201117 Fixed a memory leak - FIELD-GROUP widgets were not being deleted. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** OM 20201030 Invalid attribute API support for getters/setters. */ /* @@ -347,8 +354,7 @@ handle window = getParentHandle(); if (!window.isUnknown()) { - sibling = HandleChain.lastResource((HandleChain) window.unwrap(), - LegacyResource.WINDOW); + sibling = HandleChain.lastResource((HandleChain) window.unwrap(), LegacyResource.WINDOW); } } @@ -379,10 +385,10 @@ */ public character getScreenValue() { - handle.invalidAttribute("SCREEN-VALUE", asWidgetHandle()); + handle.invalidAttribute("SCREEN-VALUE", asWidgetHandle(), false); return new character(); } - + /** * Set the current value in the screen buffer of the backing data for * this widget. If the given value is null then this @@ -392,7 +398,8 @@ * The new value for the widget, use null to set * the value as uninitialized. */ - public void setScreenValue(character value) + @Override + protected void setScreenValueInt(character value) { handle.readOnlyError(asWidgetHandle(), "SCREEN-VALUE"); } @@ -612,7 +619,7 @@ return; } - if (getAttr("realized", () -> config.realized) && + if (getAttr("realized", () -> config.realized, true) && btn.getAttr("frameId", () -> btn.config.frameId) == -1) { ErrorManager.recordOrShowError(4040, @@ -695,8 +702,8 @@ } ButtonWidget btn = (ButtonWidget) value; - if (btn.getAttr("frameId ", () -> btn.config.frameId) != -1 && - btn.getAttr("frameId ", () -> btn.config.frameId) != getId()) + if (btn.getAttr("frameId", () -> btn.config.frameId) != -1 && + btn.getAttr("frameId", () -> btn.config.frameId) != getId()) { ErrorManager.recordOrShowError(4078, String.format( @@ -707,7 +714,7 @@ return; } - if (getAttr("realized", () -> config.realized) + if (getAttr("realized", () -> config.realized, true) && btn.getAttr("frameId", () -> btn.config.frameId) == -1) { ErrorManager.recordOrShowError(4040, @@ -945,7 +952,7 @@ @Override public integer getDown() { - return new integer(getAttr("down", () -> config.down)); + return new integer(getAttr("down", () -> config.down, true)); } /** @@ -1283,12 +1290,12 @@ // TODO: needed? // if (scroll) // { -// if (getAttr("virtualHeightChars ", () -> config.virtualHeightChars )== BaseConfig.INV_COORD) +// if (getAttr("virtualHeightChars", () -> config.virtualHeightChars, true) == BaseConfig.INV_COORD) // { // config.virtualHeightChars = _getHeightChars().doubleValue(); // } // -// if (getAttr("virtualWidthChars ", () -> config.virtualWidthChars )== BaseConfig.INV_COORD) +// if (getAttr("virtualWidthChars", () -> config.virtualWidthChars, true) == BaseConfig.INV_COORD) // { // config.virtualWidthChars = _getWidthChars().doubleValue(); // } @@ -1333,7 +1340,7 @@ // save this, as it will be overwritten by the super call GenericFrame owner = this.frame; - if (owner != null && getAttr("realized", () -> config.realized)) + if (owner != null && getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4078, String.format( @@ -1395,8 +1402,7 @@ // related frames must always land on the same top-level window config.windowID = frame.getFrameWidget().config.windowID; - - pushScreenDefinition(); + pushWidgetAttr("windowID" , config.windowID); } } @@ -1505,7 +1511,7 @@ @Override public void setBox(boolean value) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -1812,7 +1818,7 @@ */ protected boolean isRootFrame() { - return getAttr("frameId ", () -> config.frameId )== -1 || getAttr("frameId ", () -> config.frameId )== getId(); + return getAttr("frameId", () -> config.frameId )== -1 || getAttr("frameId", () -> config.frameId )== getId(); } /** @@ -1897,7 +1903,7 @@ // remove the frame, etc frame.frameCleanup(); - if (dynamic || !getAttr("realized", () -> config.realized)) + if (dynamic || !getAttr("realized", () -> config.realized, true)) { LogicalTerminal.deregisterFrame(getId()); } @@ -1909,6 +1915,9 @@ } ConfigManager.getInstance().removeWidgetConfig(config.id); + + // delete all registered field-groups + frame.deleteFieldGroups(); } /** @@ -2189,7 +2198,7 @@ { if (!LogicalTerminal.isChui()) // GUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog, always 0 + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog, always 0 { return new integer(0); } @@ -2231,7 +2240,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog, NOP + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog, NOP { return; } @@ -2260,7 +2269,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog { return new integer(0); // Always 0 } @@ -2302,7 +2311,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog { if (!isDialogBox()) // Frame, NOP { @@ -2330,7 +2339,7 @@ { if (!LogicalTerminal.isChui()) // GUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog, always 0 + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog, always 0 { return new integer(0); } @@ -2372,7 +2381,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog { return; } @@ -2397,7 +2406,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog { // 0 unless 4GL is earlier than 11.x, then ? (unknown) boolean useZero = EnvironmentOps.versionNumber() >= 11.0f; @@ -2441,7 +2450,7 @@ } else // ChUI behavior depends on whether realized or not { - if (!getAttr("realized", () -> config.realized)) // Unrealized frame/dialog, NOP + if (!getAttr("realized", () -> config.realized, true)) // Unrealized frame/dialog, NOP { return; } @@ -2862,7 +2871,7 @@ // by the runtime // - frame is realized: the new value is ignored - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { return; } === modified file 'src/com/goldencode/p2j/ui/GenericFrame.java' --- src/com/goldencode/p2j/ui/GenericFrame.java 2020-10-07 11:19:20 +0000 +++ src/com/goldencode/p2j/ui/GenericFrame.java 2021-01-27 17:52:59 +0000 @@ -2,7 +2,7 @@ ** Module : GenericFrame.java ** Abstract : implementation of UI frames using dynamic proxy feature ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23332 Created initial version. @@ -891,6 +891,22 @@ ** EVL 20200828 Added method to change internal frame name variable. ** SBI 20200916 Changed stopEditingMode for the case of UPDATE with editing phrase, ** changed continueEditing and fixed setWorker (refs: #4804-61). +** GES 20201010 Pushed processing into screen-buffer to improve encapsulation. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201112 Avoid pushing the entire frame definition when setting the FRAME or PARENT +** attribute. +** CA 20201117 Fixed a memory leak - FIELD-GROUP widgets were not being deleted. +** SVL 20210105 When the state of a shared frame is restored, call pushScreenDefinition only +** once per frame. This eliminates flickering in ChUI. +** SVL 20210112 Do not reset ENTERED flag when exiting an EDITING cycle. +** CA 20210111 If the widget rejects the SCREEN-VALUE, do not update the local cache in the +** ScreenBuffer. +** CA 20210123 For DATE, DATETIME and DATETIME-TZ, parseScreenValue must yield an unknown date +** instead of an empty string, when SCREEN-VALUE is empty. +** For CHARACTER values, parseScreenValue must use the default format, but only if +** the widget supports FORMAT (EDITOR does not support this attribute). +** CA 20210127 Fixed a problem in DOWN frames - the literal field-group gets reconstructed, and +** it must be forced to be deleted. */ /* @@ -1371,9 +1387,15 @@ @Override public void finished() { + boolean update = false; for (GenericWidget gw : mFrame.widgets) { - gw.restoreSharedState(); + update = gw.restoreSharedState() || update; + } + + if (update) + { + mFrame.pushScreenDefinition(); } } @@ -1409,7 +1431,7 @@ */ public logical validateFields(boolean enabledOnly) { - if (!frame.getAttr("realized", () -> frame.config.realized)) + if (!frame.getAttr("realized", () -> frame.config.realized, true)) { return new logical(true); } @@ -2694,7 +2716,7 @@ @Override public void displayUnlessHidden() { - if (frame.getAttr("hidden", () -> frame.config.hidden)) + if (frame.getAttr("hidden", () -> frame.config.hidden, true)) { return; } @@ -3475,7 +3497,7 @@ { int wid = widget.getId(); - if (frameDef.isSideLabel(wid)) + if (frameDef != null && frameDef.isSideLabel(wid)) { GenericWidget parent = getWidgetForId(frameDef.getSideLabelParent(wid)); parent.setLabel(value); @@ -3502,8 +3524,16 @@ } // some widgets like editor do not have the format option - BaseDataType val = parseScreenValue(type, value, widget.hasFormat() ? widget._getFormat() - : null); + String format = null; + if (widget.hasFormat()) + { + format = widget._getFormat(); + if (format == null && type.equals(character.class)) + { + format = new character().defaultFormatString(); + } + } + BaseDataType val = parseScreenValue(type, value, format); // set the value in the screen buffer for this widget if (widget.setScreenValue(frameBuf, val, false)) @@ -3512,10 +3542,10 @@ // refresh the screen LogicalTerminal.refreshFrameWidget(this, wid, val); + + // update local cache + frameBuf.putScreenValue(wid, val); } - - // update local cache - frameBuf.putScreenValue(wid, val); } } @@ -3571,17 +3601,17 @@ } else if (type.equals(datetimetz.class)) { - result = isEmptyFormat(value.toStringMessage(), "-/:.+") ? new character("") + result = isEmptyFormat(value.toStringMessage(), "-/:.+") ? new datetimetz() : new datetimetz(value); } else if (type.equals(datetime.class)) { - result = isEmptyFormat(value.toStringMessage(), "-/:.+") ? new character("") + result = isEmptyFormat(value.toStringMessage(), "-/:.+") ? new datetime() : new datetime(value); } else if (type.equals(date.class)) { - result = isEmptyFormat(value.toStringMessage(), "-/") ? new character("") + result = isEmptyFormat(value.toStringMessage(), "-/") ? new date() : new date(value); } else if (type.equals(decimal.class)) @@ -6118,7 +6148,7 @@ @Override public void setDown(Resolvable down) { - if (frame.getAttr("realized", () -> frame.config.realized)) + if (frame.getAttr("realized", () -> frame.config.realized, true)) { // this is a no-op in case of shared frames - the DOWN value is inherited from the source // frame. DOWN can only be overridden by explicitly setting the FRAME's DOWN attribute @@ -7737,10 +7767,10 @@ } } } - // It changes the ENTERED state so it is called after copyFromScreenBuffer + if (ids != null) { - LogicalTerminal.disable(this, ids); + LogicalTerminal.disable(this, ids, false); } } @@ -7819,7 +7849,7 @@ for (GenericWidget widget : widgets) { - if (widget.getAttr("enabled", () -> widget.config.enabled)) + if (widget.getAttr("enabled", () -> widget.config.enabled, true)) { // TODO: this needs more testing for ChUI - some color state gets cleared (like INPUT // for editable fill-in) when widgets are enabled. @@ -8686,6 +8716,36 @@ } /** + * Remove the specified widget from this frame. + * + * @param widget + * The widget to remove. + */ + public void removeWidget(GenericWidget widget) + { + List> newWidgets = new ArrayList<>(widgets.length); + for (int i = 0; i < widgets.length; i++) + { + if (widgets[i] != widget) + { + newWidgets.add(widgets[i]); + } + } + + widgets = newWidgets.toArray(new GenericWidget[0]); + + Iterator>> iter = n2w.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry> entry = iter.next(); + if (entry.getValue() == widget) + { + iter.remove(); + } + } + } + + /** * Adding new dynamic widget to the current frame. * * @param dynWidget The widget to be added to current frame. @@ -8728,8 +8788,7 @@ // refresh frame definition if exists // but if for the given ID we already have definition - skip this step - if (frameDef != null && - (!replaceExisted || frameDef.getConfig(dynWidget.getId()) == null)) + if (frameDef != null && (!replaceExisted || frameDef.getConfig(dynWidget.getId()) == null)) { frameDef.addConfig(dynWidget.config()); if (frameBuf != null) @@ -8738,7 +8797,7 @@ } } - pushScreenDefinition(); + LogicalTerminal.attachRuntimeWidget(this, dynWidget); if (!this.getTitle().isUnknown() && dynWidget instanceof BaseEntity) { @@ -8827,13 +8886,7 @@ if (pushScreenDefinitions && !frame.deleted) { - if (frameDef == null) - { - initScreenDefinition(); - } - - frameDef.addDeletedWidget(dynId); - pushScreenDefinition(); + LogicalTerminal.deleteDynamicWidget(this, dynWidget); } } @@ -10549,11 +10602,7 @@ { if (headers != null) { - for (int i = 0; i < headers.length; i++) - { - frameBuf.putHeaderValue(headers[i].getWidget().getId(), - headers[i].get()); - } + frameBuf.allocateHeaders(headers); } frameBuf.setCurrentStatement(currentStatement); @@ -10839,7 +10888,7 @@ } } - if (!frame.getAttr("hidden", () -> frame.config.hidden)) + if (!frame.getAttr("hidden", () -> frame.config.hidden, true)) { // make the frame visible frame.config.wasVisible = frame.config.visible; @@ -10906,7 +10955,7 @@ } // make the frame visible - frame.config.wasVisible = frame.getAttr("visible", () -> frame.config.visible); + frame.config.wasVisible = frame.getAttr("visible", () -> frame.config.visible, true); frame._setVisible(true); // set the hidden flag to OFF frame._setHidden(false); @@ -11080,7 +11129,7 @@ */ private void displayUnlessHidden(FrameElement[] data, boolean windowClause, handle hWin) { - if (frame.getAttr("hidden", () -> frame.config.hidden)) + if (frame.getAttr("hidden", () -> frame.config.hidden, true)) { sendWidgetScreenValue(data); @@ -11732,18 +11781,9 @@ ConfigHelper.setModified(frame.config, true); - // set all widgets to changed - Iterator iter = buff.getWidgetIDIterator(); - ArrayList ids = new ArrayList(); - - while (iter.hasNext()) - { - Integer id = iter.next(); - ids.add(id); - buff.setState(id, ScreenBuffer.CHANGED); - } - - LogicalTerminal.view(this, Utils.integerCollectionToPrimitive(ids)); + // markAllChanged() will not return null, this is intentional since LT.view() treats that as "all + // widgets" instead of "no widgets"; so if there are no widgets the result will be a 0 sized array + LogicalTerminal.view(this, buff.markAllChanged()); } /** @@ -12569,7 +12609,7 @@ @Override public void displayUnlessHiddenAndDown(FrameElement[] data) { - if (frame.getAttr("hidden", () -> frame.config.hidden)) + if (frame.getAttr("hidden", () -> frame.config.hidden, true)) { sendWidgetScreenValue(data); @@ -12597,7 +12637,7 @@ */ public void displayUnlessHiddenAndDown() { - if (frame.getAttr("hidden", () -> frame.config.hidden)) + if (frame.getAttr("hidden", () -> frame.config.hidden, true)) { return; } @@ -12654,7 +12694,7 @@ } GenericWidget widget = el.getWidget(); - if (!widget.getAttr("hidden", () -> widget.config.hidden)) + if (!widget.getAttr("hidden", () -> widget.config.hidden, true)) { el.getWidget().setScreenValue(el.get()); } @@ -12860,6 +12900,32 @@ } /** + * Delete all field-groups created for this frame. + */ + void deleteFieldGroups() + { + if (fieldGroup != null) + { + fieldGroup.delete(); + } + + if (literalFieldGroup != null) + { + literalFieldGroup.delete(); + } + + for (FieldGroup fg : rows2fg.values()) + { + fg.delete(); + } + + if (frame != null && frame.getBackground() != null) + { + frame.getBackground().delete(); + } + } + + /** * This method prints field-groups of the frame and is needed only for debugging purposes. * * Here is a sample output (each triplet group is for a distinct call): @@ -12948,6 +13014,7 @@ // the last iteration in recursy needs this to restore literal group to initial state if (literalFieldGroup != null) { + literalFieldGroup.forceDelete(); literalFieldGroup = initLiteralFG(); } return; @@ -12997,7 +13064,15 @@ { fg = tmpFg; } - rows2fg.put(nextRow, fg); + FieldGroup oldFg = rows2fg.put(nextRow, fg); + if (oldFg != null) + { + oldFg.delete(); + if (oldFg == fieldGroup) + { + fieldGroup = fg; + } + } currentFieldGroup = fg; updateFieldGroups(lines, nextRow, down, --realstep); === modified file 'src/com/goldencode/p2j/ui/GenericWidget.java' --- src/com/goldencode/p2j/ui/GenericWidget.java 2020-10-07 18:32:15 +0000 +++ src/com/goldencode/p2j/ui/GenericWidget.java 2020-12-05 02:01:57 +0000 @@ -2,9 +2,9 @@ ** Module : GenericWidget.java ** Abstract : common superclass for all server side widgets ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- +** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23492 Created initial version. ** 002 GES 20060114 @23910 Removed totally useless screen-value attr. ** 003 NVS 20060114 @24094 Implemented hide() and view() methods. @@ -277,6 +277,31 @@ ** HC 20201004 Performance optimizations of server-client config state ** synchronization. ** AIL 20201007 Made setAttr satisfy constraints from child classes by using canPushWidgetAttr. +** HC 20201010 Implemented selective config flushing. +** HC 20201013 Server-pushed config field changes are serialized with field ids instead of +** names. +** GES 20201012 Reworked putWidgetValue() to reduce calls. +** EVL 20201019 Optimized getId() call. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** RFB 20201106 Added TextWidget to the list of dynamic widgets that can accept a label. #4873 +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this +** code. +** CA 20201126 In some cases of internal usage of an attribute, the widget must not be realized +** (e.g. COMBO-BOX:LIST-ITEMS relies internally on ADD-FIRST, and it must not +** realize the widget). +** SVL 20210105 Removed pushScreenDefinition from restoreSharedState. It should be called +** externally. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. +** CA 20210123 getScreenValue must return an empty string for an unknown DATE, DATETIME or +** DATETIME-TZ, and unknown value otherwise. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. +** OM 20201030 Invalid attribute API support for getters/setters. +** OM 20201203 Fixed handling of READ/ONLY attributes. */ /* @@ -338,7 +363,6 @@ import com.goldencode.p2j.persist.*; import com.goldencode.p2j.util.*; -import java.math.*; import java.util.*; import java.util.function.*; @@ -365,6 +389,15 @@ /** Flag indicating the resource was deleted. */ protected boolean deleted = false; + /** Flag indicating if the widget must be realized on attribute access. */ + protected boolean realizeOnAttributeAccess = true; + + /** + * Flag indicating if the {@link #setScreenValue} is being called from the converted code or from within + * FWD. + */ + protected boolean internalScreenValueUsage = true; + /** The validation expression to use to test proposed edits for this widget. */ private ValidationExpr valexp = null; @@ -426,9 +459,16 @@ /** When {@code true} config field changes batching is disabled. */ private static boolean configBatchingDisabled = false; + /** When {@code true} selective config flushing is disabled. */ + private static boolean selectiveConfigFlushDisabled = false; + + /** The cached config definition for the widget's config */ + private WidgetConfigDef configDef; + static { configBatchingDisabled = System.getProperty("config.batching.disabled") != null; + selectiveConfigFlushDisabled = System.getProperty("selective.config.flush.disabled") != null; } /** @@ -660,7 +700,8 @@ @Override public int getId() { - return getAttr("id ", () -> config.id )== null ? -1 : getAttr("id", () -> config.id).asInt(); + // generic ID getter for server driven constant ID + return config == null || config.id == null ? -1 : config.id.asInt(); } /** @@ -674,7 +715,7 @@ public void setId(int id) { // TODO: for now, ID's are not allowed to be set if the widget's ID is already set - if (getAttr("id ", () -> config.id )!= null) + if (getId() != -1) { return; } @@ -773,7 +814,7 @@ */ public logical isAutoReturn() { - handle.invalidAttribute("AUTO-RETURN", asWidgetHandle()); + handle.invalidAttribute("AUTO-RETURN", asWidgetHandle(), false); return new logical(); } @@ -942,7 +983,7 @@ @Override public character getColumnLabel() { - invalidAttribute("COLUMN-LABEL"); + invalidAttribute("COLUMN-LABEL", false); return new character(); } @@ -1133,7 +1174,7 @@ */ public String _getFormat() { - return config.getDynamicFormat(); + return hasFormat() ? config.getDynamicFormat() : null; } /** @@ -1330,7 +1371,7 @@ */ public boolean _isHidden() { - return getAttr("hidden", () -> config.hidden); + return getAttr("hidden", () -> config.hidden, true); } /** @@ -1361,7 +1402,8 @@ } // check if the parent frame is set and not visible - if (!getAttr("realized", () -> config.realized) && (frame == null || !frame.getFrameWidget()._isVisible())) + if (!getAttr("realized", () -> config.realized, true) && + (frame == null || !frame.getFrameWidget()._isVisible())) { config.hidden = hidden; @@ -1464,37 +1506,10 @@ @Override public void setLabel(String label) { - // TODO: Some refactoring is required here to avoid instance check and class casting - // the code below is not very good from the architecture perspective. Ideally we need - // to provide some protected method here allowing the particular widget to override it - // and perform widget specific actions, for example ButtonWidget or ToggleBox - // in this case. - - // dynamic widgets can not accept labels, except BUTTON and TOGGLE-BX, BROWSE-COLUMN - if (_dynamic() && - !(this instanceof ButtonWidget) && - !(this instanceof ToggleBoxWidget) && - !(this instanceof BrowseColumnWidget)) + if (validateLabelAssignment()) { - if (this instanceof FrameWidget) - { - // form the message and display it - ErrorManager.displayError(4052, "LABEL is not a settable attribute for FRAME widget"); - } - else - { - StringBuilder err = new StringBuilder(); - // form the message - err.append("Unable to set LABEL because no LABEL-HANDLE on the ") - .append(type()) - .append(" widget"); - // and display it - ErrorManager.displayError(4072, err.toString()); - } - return; + setLabelInt(label); } - - setLabelInt(label); } /** @@ -1554,7 +1569,7 @@ @Override public logical isModified() { - return new logical(getAttr("modified", () -> config.modified)); + return new logical(getAttr("modified", () -> config.modified, true)); } /** @@ -2033,7 +2048,7 @@ @Override public logical isSensitive() { - return new logical(getAttr("enabled", () -> config.enabled)); + return new logical(getAttr("enabled", () -> config.enabled, true)); } /** @@ -2095,7 +2110,7 @@ } if (!frameLock && - getAttr("visible", () -> config.visible) && + getAttr("visible", () -> config.visible, true) && frame != null && frame.getFrameWidget().allFramesVisible()) { @@ -2454,7 +2469,7 @@ */ public boolean _isVisible() { - return getAttr("visible", () -> config.visible); + return getAttr("visible", () -> config.visible, true); } /** @@ -3156,19 +3171,29 @@ * the value as uninitialized. */ @Override - public void setScreenValue(character value) + public final void setScreenValue(character value) { - // EDITOR/SELECTION-LIST/TOGGLE-BOX: setter changes the MODIFIED to TRUE, even if the - // value doesn't change + boolean old = internalScreenValueUsage; + internalScreenValueUsage = false; + try + { + setScreenValueInt(value); + } + finally + { + internalScreenValueUsage = old; + } + } - if (frame != null) - { - frame.setScreenValue(this, value); - } - else - { - pendingScreenValue = (character) value.duplicate(); - } + /** + * Sets the SCREEN-VALUE writable attribute. + * + * @param screenValue + * The new value for the SCREEN-VALUE attribute. + */ + public final void setScreenValue(String screenValue) + { + setScreenValue(new character(screenValue)); } /** @@ -3194,9 +3219,7 @@ if (bdt.isUnknown()) { - character chRet = new character(bdt.getClass().isAssignableFrom(date.class) ? "" : "?"); - chRet.setUnknown(); - return chRet; + return bdt.getClass().isAssignableFrom(date.class) ? new character("") : new character(); } // format the value @@ -3226,10 +3249,9 @@ { int widgetID = getId(); - if (frameBuf.isIdValid(widgetID)) + if (frameBuf.putWidgetValue(widgetID, value)) { - frameBuf.putWidgetValue(widgetID, value); - config.modified = true; + config.modified = !internalScreenValueUsage; } return true; @@ -3362,18 +3384,34 @@ } /** - * API needed to implement read-only attribute assignment (a 4GL - * "feature"). + * API needed to implement read-only attribute assignment (a 4GL "feature"). * * @param attribute * The attribute's name. */ @Override + @Deprecated public void readOnlyError(String attribute) { - handle.readOnlyError(asWidgetHandle(), attribute); - } - + handle.readOnlyError(asWidgetHandle(), attribute, handle.UNKNOWN_ARGUMENT); + } + + /** + * Shows a specific error, as this read-only {@code attribute} was used on the right-side of an assignment. + * If the {@code expr} is evaluated to {@code ?} (unknown value) and this attribute does not support it, + * a specific error is raised. + * + * @param attribute + * The read-only attribute. + * @param expr + * The expression whose value was attempted to be assigned to READ-ONLY attribute. + */ + @Override + public void readOnlyError(String attribute, Object expr) + { + handle.readOnlyError(asWidgetHandle(), attribute, expr); + } + /** * Gets the integer value of the window containing the widget. * @@ -3816,7 +3854,7 @@ @Override public void openPopup() { - if (getAttr("popupMenuId ", () -> config.popupMenuId )!= -1) + if (getAttr("popupMenuId", () -> config.popupMenuId )!= -1) { LogicalTerminal.getClient().openPopup(getId()); } @@ -3994,7 +4032,7 @@ */ public boolean _isRealized() { - return getAttr("realized", () -> config.realized); + return getAttr("realized", () -> config.realized, true); } /** @@ -4073,7 +4111,7 @@ @Override public void setContextHelpId(int id) { - if (getAttr("contextHelpId ", () -> config.contextHelpId )== id) + if (getAttr("contextHelpId", () -> config.contextHelpId )== id) { return; } @@ -4523,6 +4561,7 @@ * * @return TRANSPARENT current value. */ + @LegacyAttribute(name = "TRANSPARENT", ignore = true) @Override public logical getTransparent() { @@ -4536,6 +4575,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "TRANSPARENT", setter = true, ignore = true) @Override public void setTransparent(boolean flag) { @@ -4548,6 +4588,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "TRANSPARENT", setter = true, ignore = true) @Override public void setTransparent(logical flag) { @@ -4560,6 +4601,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true, ignore = true) @Override public void setReadOnly(boolean r) { @@ -4572,6 +4614,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true, ignore = true) @Override public void setReadOnly(logical r) { @@ -4583,6 +4626,7 @@ * * @return STRETCH-TO-FIT current value. */ + @LegacyAttribute(name = "STRETCH-TO-FIT", ignore = true) @Override public logical getStretchToFit() { @@ -4596,6 +4640,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "STRETCH-TO-FIT", setter = true, ignore = true) @Override public void setStretchToFit(boolean flag) { @@ -4608,6 +4653,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "STRETCH-TO-FIT", setter = true, ignore = true) @Override public void setStretchToFit(logical flag) { @@ -4619,6 +4665,7 @@ * * @return RETAIN-SHAPE current value. */ + @LegacyAttribute(name = "RETAIN-SHAPE", ignore = true) @Override public logical getRetainShape() { @@ -4632,6 +4679,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "RETAIN-SHAPE", setter = true, ignore = true) @Override public void setRetainShape(boolean flag) { @@ -4644,6 +4692,7 @@ * @param flag * New value for the attribute. */ + @LegacyAttribute(name = "RETAIN-SHAPE", setter = true, ignore = true) @Override public void setRetainShape(logical flag) { @@ -4658,6 +4707,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public boolean loadImage(String name) { @@ -4677,6 +4727,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public boolean loadImage(String name, long xOffset) { @@ -4698,6 +4749,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public boolean loadImage(String name, long xOffset, long yOffset) { @@ -4721,6 +4773,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public boolean loadImage(String name, long xOffset, long yOffset, long width) { @@ -4746,6 +4799,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public boolean loadImage(String name, long xOffset, long yOffset, long width, long height) { @@ -4761,6 +4815,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public logical loadImage(character name) { @@ -4780,6 +4835,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public logical loadImage(character name, int64 xOffset) { @@ -4801,6 +4857,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public logical loadImage(character name, int64 xOffset, int64 yOffset) { @@ -4824,6 +4881,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public logical loadImage(character name, int64 xOffset, int64 yOffset, int64 width) { @@ -4849,6 +4907,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE", ignore = true) @Override public logical loadImage(character name, int64 xOffset, int64 yOffset, int64 width, int64 height) { @@ -4861,6 +4920,7 @@ * * @return CONVERT-3D-COLORS current value. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", ignore = true) @Override public logical getConvert3D() { @@ -4874,6 +4934,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true, ignore = true) @Override public void setConvert3D(boolean flag) { @@ -4886,6 +4947,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true, ignore = true) @Override public void setConvert3D(logical flag) { @@ -4929,7 +4991,24 @@ */ public T getAttr(String fname, Supplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public T getAttr(String fname, Supplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -4949,7 +5028,24 @@ */ public boolean getAttr(String fname, BooleanSupplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public boolean getAttr(String fname, BooleanSupplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -4969,7 +5065,24 @@ */ public char getAttr(String fname, CharSupplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public char getAttr(String fname, CharSupplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -4989,7 +5102,24 @@ */ public int getAttr(String fname, IntSupplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public int getAttr(String fname, IntSupplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -5009,7 +5139,24 @@ */ public long getAttr(String fname, LongSupplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public long getAttr(String fname, LongSupplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -5029,7 +5176,24 @@ */ public double getAttr(String fname, DoubleSupplier getter) { - if (!configBatchingDisabled) + return getAttr(fname, getter, false); + } + + /** + * Returns config field value. + * + * @param fname + * Config field name. + * @param getter + * Supplier responsible for retrieving the field value from the corresponding config instance. + * @param flush + * Flag to make attribute flush before calling the getter. + * + * @return Config field value. + */ + public double getAttr(String fname, DoubleSupplier getter, boolean flush) + { + if (!configBatchingDisabled && flush) { getLogicalTerminal().flushEnqueuedWidgetAttrs(); } @@ -5065,7 +5229,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5101,7 +5265,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5137,7 +5301,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5173,7 +5337,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5209,7 +5373,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5245,7 +5409,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5281,7 +5445,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5317,7 +5481,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, newValue); + enqueuWidgetAttr(config.id.asInt(), fname, newValue); } else { @@ -5363,7 +5527,7 @@ if (!configBatchingDisabled) { - getLogicalTerminal().enqueuWidgetAttr(config.id.asInt(), fname, value); + enqueuWidgetAttr(config.id.asInt(), fname, value); } else { @@ -5391,10 +5555,9 @@ if (!configBatchingDisabled) { - LogicalTerminal lt = getLogicalTerminal(); for (int i = 0; i < values.length; i++) { - lt.enqueuWidgetAttr(config.id.asInt(), fnames[i], values[i]); + enqueuWidgetAttr(config.id.asInt(), fnames[i], values[i]); } } else @@ -5433,12 +5596,14 @@ /** * Restore the shared state for this widget: HELP and VALEXP. + * + * @return true if the shared state has been restored. */ - void restoreSharedState() + boolean restoreSharedState() { if (!sharedStateSaved) { - return; + return false; } sharedStateSaved = false; @@ -5450,14 +5615,14 @@ valexp = saveValexp; valmsg = saveValmsg; - pushScreenDefinition(); - saveIsDefaultHelp = false; saveDefaultHelp = null; saveConfigHelp = null; saveValexp = null; saveValmsg = null; + + return true; } /** @@ -5571,6 +5736,26 @@ return false; } + return true; + } + + /** + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + super.delete(); + // removing dynamic widget from associated frame if (_dynamic()) { @@ -5583,13 +5768,16 @@ LogicalTerminal.deregisterWidget(getId()); } } + // else: detach it + else + { + LogicalTerminal.deregisterWidget(getId()); + } // remove the config for this widget - ConfigManager.getInstance().removeWidgetConfig(getAttr("id", () -> config.id)); + ConfigManager.getInstance().removeWidgetConfig(config.id); deleted = true; - - return true; } /** @@ -5648,7 +5836,7 @@ @Override public void realize() { - if (getAttr("wasRealized", () -> config.wasRealized)) + if (getAttr("wasRealized", () -> config.wasRealized, true)) { return; } @@ -5764,8 +5952,15 @@ */ protected boolean canAccess(String attr) { - realize(); - if (!getAttr("wasRealized", () -> config.wasRealized)) + if (realizeOnAttributeAccess) + { + realize(); + } + else + { + return true; + } + if (!getAttr("wasRealized", () -> config.wasRealized, true)) { ErrorManager.recordOrShowError(4104, String.format( @@ -5943,8 +6138,8 @@ */ protected boolean moveToWorker(boolean top) { - Boolean btop = top ? LogicalTerminal.moveToTop(getAttr("id", () -> config.id).asInt()) - : LogicalTerminal.moveToBottom(getAttr("id", () -> config.id).asInt()); + int wid = getId(); + Boolean btop = top ? LogicalTerminal.moveToTop(wid) : LogicalTerminal.moveToBottom(wid); if (btop != null && btop.booleanValue()) { @@ -5982,6 +6177,48 @@ } /** + * Implements legacy validations performed at side-label assignment. + * + * @return {@code true} if the validation succeeds. + */ + protected boolean validateLabelAssignment() + { + // TODO: Some refactoring is required here to avoid instance check and class casting + // the code below is not very good from the architecture perspective. Ideally we need + // to provide some protected method here allowing the particular widget to override it + // and perform widget specific actions, for example ButtonWidget or ToggleBox + // in this case. + + // dynamic widgets can not accept labels, except BUTTON, TOGGLE-BX, BROWSE-COLUMN, FILL-IN and TEXT + if (_dynamic() && + !(this instanceof ButtonWidget) && + !(this instanceof ToggleBoxWidget) && + !(this instanceof BrowseColumnWidget) && + !(this instanceof FillInWidget) && + !(this instanceof TextWidget)) + { + if (this instanceof FrameWidget) + { + // form the message and display it + ErrorManager.displayError(4052, "LABEL is not a settable attribute for FRAME widget"); + } + else + { + StringBuilder err = new StringBuilder(); + // form the message + err.append("Unable to set LABEL because no LABEL-HANDLE on the ") + .append(type()) + .append(" widget"); + // and display it + ErrorManager.displayError(4072, err.toString()); + } + return false; + } + + return true; + } + + /** * Sets the label text. * * @param label @@ -6031,8 +6268,52 @@ { return !(deleted || frame == null || frame.isInsideSetup()); } + + /** + * Set the current value in the screen buffer of the backing data for + * this widget. If the given value is null then this + * widget will be set to the uninitialized value. + * + * @param value + * The new value for the widget, use null to set + * the value as uninitialized. + */ + protected void setScreenValueInt(character value) + { + // EDITOR/SELECTION-LIST/TOGGLE-BOX: setter changes the MODIFIED to TRUE, even if the + // value doesn't change + + if (frame != null) + { + frame.setScreenValue(this, value); + } + else + { + pendingScreenValue = (character) value.duplicate(); + } + } /** + * Enqueues widget attribute assignment. + * + * @param widgetId + * Widget id. + * @param fname + * Field name. + * @param value + * New value. + */ + private void enqueuWidgetAttr(int widgetId, String fname, Object value) + { + if (this.configDef == null) + { + configDef = ConfigManager.getInstance().getConfigDef(config.getClass()); + } + + getLogicalTerminal().enqueuWidgetAttr(widgetId, fname, configDef.fieldIds.get(fname), value); + } + + /** * Processes validation rules, error checking and any other user-defined constraint processing * that needs to be applied. This is used to check if a proposed update to an object will * pass validation. If the method silently returns then all tests have passed, otherwise an === modified file 'src/com/goldencode/p2j/ui/GuiCellAttributes.java' --- src/com/goldencode/p2j/ui/GuiCellAttributes.java 2020-09-17 11:46:00 +0000 +++ src/com/goldencode/p2j/ui/GuiCellAttributes.java 2020-11-19 21:19:17 +0000 @@ -7,6 +7,7 @@ ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 SVL 20170814 Initial version. ** 002 IAS 20200914 Re-work (de)serialization +** SVL 20201119 Added FONT attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -78,6 +79,9 @@ /** FGCOLOR attribute. */ public Integer fgColor = null; + /** FONT attribute. */ + public Integer font = null; + /** Default constructor */ public GuiCellAttributes() { @@ -98,6 +102,7 @@ { writeInteger(out, bgColor); writeInteger(out, fgColor); + writeInteger(out, font); } /** @@ -116,5 +121,6 @@ { bgColor = readInteger(in); fgColor = readInteger(in); + font = readInteger(in); } } === modified file 'src/com/goldencode/p2j/ui/HtmlBrowserWidget.java' --- src/com/goldencode/p2j/ui/HtmlBrowserWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/HtmlBrowserWidget.java 2020-10-21 23:01:05 +0000 @@ -10,6 +10,7 @@ ** 003 CA 20200304 BeforeNavigate2 event is optional. ** 004 HC 20200326 Added required method overload for openWebPage and removed openWebPage ** overloads with raw Java parameter types. +** 005 EVL 20201021 More optimization for getAttr() usage or widget ID getting. */ /* @@ -115,9 +116,9 @@ }; /** - * Default constructor. * @author ca - * - + * Default constructor. + * + * @author ca */ public HtmlBrowserWidget() { @@ -202,8 +203,8 @@ @Override public character getResourceBase() { - return getAttr("resourceBase", () -> config.resourceBase) == null ? new character() : - new character(getAttr("resourceBase", () -> config.resourceBase).toString()); + return getAttr("resourceBase", () -> (config.resourceBase == null ? new character() : + new character(config.resourceBase.toString()))); } /** === modified file 'src/com/goldencode/p2j/ui/ImageWidget.java' --- src/com/goldencode/p2j/ui/ImageWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/ImageWidget.java 2021-01-15 20:55:46 +0000 @@ -24,6 +24,7 @@ ** 012 CA 20180313 Added getters for BUTTON image related attributes. ** 013 SBI 20180705 Changed loadImageInt in order to clear the current image if an unknown image ** is loaded via LOAD-IMAGE(?). +** 014 VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. */ /* ** This program is free software: you can redistribute it and/or modify @@ -163,6 +164,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true) @Override public void setConvert3D(boolean flag) { @@ -180,6 +182,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS", setter = true) @Override public void setConvert3D(logical flag) { @@ -194,6 +197,7 @@ * * @return CONVERT-3D-COLORS current value. */ + @LegacyAttribute(name = "CONVERT-3D-COLORS") @Override public logical getConvert3D() { @@ -206,6 +210,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "TRANSPARENT", setter = true) @Override public void setTransparent(boolean flag) { @@ -223,6 +228,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "TRANSPARENT", setter = true) @Override public void setTransparent(logical flag) { @@ -237,6 +243,7 @@ * * @return TRANSPARENT current value. */ + @LegacyAttribute(name = "TRANSPARENT") @Override public logical getTransparent() { @@ -249,6 +256,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "STRETCH-TO-FIT", setter = true) @Override public void setStretchToFit(boolean flag) { @@ -266,6 +274,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "STRETCH-TO-FIT", setter = true) @Override public void setStretchToFit(logical flag) { @@ -280,6 +289,7 @@ * * @return STRETCH-TO-FIT current value. */ + @LegacyAttribute(name = "STRETCH-TO-FIT") @Override public logical getStretchToFit() { @@ -292,6 +302,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "RETAIN-SHAPE", setter = true) @Override public void setRetainShape(boolean flag) { @@ -309,6 +320,7 @@ * @param flag * New value for the option. */ + @LegacyAttribute(name = "RETAIN-SHAPE", setter = true) @Override public void setRetainShape(logical flag) { @@ -323,6 +335,7 @@ * * @return RETAIN-SHAPE current value. */ + @LegacyAttribute(name = "RETAIN-SHAPE") @Override public logical getRetainShape() { @@ -437,6 +450,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name) { @@ -455,6 +469,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset) { @@ -475,6 +490,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset) { @@ -501,6 +517,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset, long width) { @@ -529,6 +546,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public boolean loadImage(String name, long xOffset, long yOffset, long width, long height) { @@ -547,6 +565,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name) { @@ -565,6 +584,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset) { @@ -585,6 +605,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, int64 yOffset) { @@ -607,6 +628,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, int64 yOffset, int64 width) { @@ -631,6 +653,7 @@ * * @return true if operation was successful. */ + @LegacyMethod(name = "LOAD-IMAGE") @Override public logical loadImage(character name, int64 xOffset, int64 yOffset, int64 width, int64 height) { === modified file 'src/com/goldencode/p2j/ui/Keyboard.java' --- src/com/goldencode/p2j/ui/Keyboard.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/Keyboard.java 2021-01-27 17:05:13 +0000 @@ -2,7 +2,7 @@ ** Module : Keyboard.java ** Abstract : A collection of keyboard event mapping methods. ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ---------------------------Description---------------------------- ** 001 NVS 20051201 @23590 Created initial version. @@ -110,6 +110,15 @@ ** 061 SBI 20200619 Changed NODECLICK, BEFORELABELEDIT, AFTERLABELEDIT to be according with FWD ** widget events naming convention. ** HC 20200726 Initial implementation of SPREADSHEET widget and related changes. +** CA 20201011 Improved findKey and findKeyEnd performance. +** CA 20210118 Added SCROLL-NOTIFY event and SE_ROW_DISPLAY. +** CA 20210120 Added registeredEventName, used during parsing to check the event names with the +** FWD runtime. +** CA 20210120 Fixed keyCode(String) when the label is prefixed with a modifier and the actual +** label is an 'altLabel'. +** CA 20210121 Added OFF-END, OFF-HOME, DESELECT and PARENT-WINDOW-CLOSE. No runtime exists for +** them at this time. +** CA 20210127 Fixed key-code to detect extended labels. */ /* ** This program is free software: you can redistribute it and/or modify @@ -591,6 +600,13 @@ public static final String START_EDIT = "START-EDIT"; public static final String TOP_LEFT_CHANGED = "TOP-LEFT-CHANGED"; + // other event names + public static final String SCROLL_NOTIFY = "SCROLL-NOTIFY"; + public static final String OFF_END = "OFF-END"; + public static final String OFF_HOME = "OFF-HOME"; + public static final String DESELECT = "DESELECT"; + public static final String PARENT_WINDOW_CLOSE = "PARENT-WINDOW-CLOSE"; + /** Event/action names. */ private static String[] evNames1 = new String[] { @@ -638,8 +654,10 @@ TOP_LEFT_CHANGED , MOUSE_CLICK , MOUSE_UP , MOUSE_DOWN , MOUSE_MOVE , OLE_COMPLETE_DRAG , OLE_DRAG_DROP , OLE_DRAG_OVER , OLE_GIVE_FEEDBACK , - OLE_SET_DATA , OLE_START_DRAG , NODE_CLICK, - NODE_BEFORE_EDIT , NODE_AFTER_EDIT + OLE_SET_DATA , OLE_START_DRAG , NODE_CLICK , + NODE_BEFORE_EDIT , NODE_AFTER_EDIT , SCROLL_NOTIFY , + PARENT_WINDOW_CLOSE, OFF_END , OFF_HOME , + DESELECT }; /** Starting point for key action codes. */ @@ -779,6 +797,9 @@ /** synthetic event code */ public static final int SE_PREV_FRAME = eventCode(PREV_FRAME); + + /** synthetic event code */ + public static final int SE_ROW_DISPLAY = eventCode(ROW_DISPLAY); /** synthetic event code */ public static final int SE_ENTRY = eventCode(ENTRY); @@ -914,7 +935,13 @@ )); /** Stores context-local state variables. */ - private static ContextContainer work = new ContextContainer(); + private static ContextContainer work = new ContextContainer(); + + /** + * A list of all keyboards implemented in FWD - used during parsing by {@link #registeredEventName(String)}, + * to check if a certain trigger's event is known by the FWD runtime. + */ + private static Keyboard[] kbs = null; /** mapping of key functions into key labels the function is mapped to*/ private Map> f2l = new HashMap<>(); @@ -937,9 +964,15 @@ /** basic key names */ protected String[] basicKeys = new String[655]; + /** A map of each {@link #basicKeys} to their index. */ + protected Map basicKeyCodes = new HashMap<>(); + /** extended key names */ protected Map extendedKeys = new TreeMap<>(); + /** Extended labels to their keys. */ + protected Map extendedLabels = new TreeMap<>(); + /** * Constructor. * @@ -952,6 +985,46 @@ } /** + * Check if the given event name is registered in any keyboard implementation. + * + * @param event + * The event name, as passed to the trigger. + * + * @return true if {@link EventList#eventCode} can resolve this event's ID. + */ + public static boolean registeredEventName(String event) + { + if (kbs == null) + { + kbs = new Keyboard[] + { + new GuiKeyboard(), + new ChuiWindowsKeyboard(), + new ChuiLinuxKeyboard() + }; + + for (Keyboard kb : kbs) + { + work.set(kb); + kb.init(); + } + } + + for (Keyboard kb : kbs) + { + work.set(kb); + String evt = EventList.setCase(event); + Integer evtCode = EventList.eventCode(evt); + if (evtCode != null) + { + return true; + } + } + + return false; + } + + /** * Converts a named event into a numeric event code. * The conversion takes into account the fact, that the event name may * refer to (in the specified order): @@ -1096,11 +1169,37 @@ // check alternate key labels for a match if (code == -1) { + String[] prefixes = { "CTRL-", "SHIFT-", kb.bit10 + "-" }; + String prefix = ""; + l1: do + { + for (String p : prefixes) + { + if (lab.startsWith(p)) + { + prefix = prefix + p; + lab = lab.substring(p.length()); + continue l1; + } + } + + break; + } + while (true); + Integer kc = (Integer) kb.altLabel.get(lab); if (kc != null) { - code = kc.intValue(); + if (!prefix.isEmpty()) + { + String newLbl = keyLabel(kc); + return keyCode(prefix + newLbl); + } + else + { + code = kc.intValue(); + } } } @@ -1547,6 +1646,11 @@ @Override public void init() { + for (Map.Entry ke : extendedKeys.entrySet()) + { + extendedLabels.put(ke.getValue(), ke.getKey()); + } + // set "" as the keys for 0 to 655 Arrays.fill(basicKeys, ""); @@ -1841,6 +1945,8 @@ basicKeys[651] = "MOUSE-MENU-DBLCLICK"; basicKeys[652] = "MOUSE-EXTEND-DBLCLICK"; + initBasicKeyCodes(); + // filling the map of key functions with standard mappings Map standards = standardKeyFunctions(); @@ -1929,6 +2035,8 @@ basicKeys[i] = new String(ch); } + + initBasicKeyCodes(); } /** @@ -2119,16 +2227,19 @@ private int findKey(String label) { // some events have special meaning - if (osEventsCodes.containsKey(label)) - { - return osEventsCodes.get(label); - } - - for (int i = 0; i < basicKeys.length; i ++) - if (label.equals(basicKeys[i])) - return i; - - return -1; + Integer idx = osEventsCodes.get(label); + if (idx != null) + { + return idx; + } + + idx = basicKeyCodes.get(label); + if (idx == null) + { + idx = extendedLabels.get(label); + } + + return idx == null ? -1 : idx.intValue(); } /** @@ -2142,31 +2253,38 @@ */ private int findKeyEnd(String label) { - int l = label.length(); - - for (int i = 0; i < basicKeys.length; i ++) - { - if (basicKeys[i].length() == 0) - continue; - - if (label.endsWith(basicKeys[i])) + String[] prefixes = + { + "CTRL-SHIFT-" + bit10 + "-", + "CTRL-" + bit10 + "-", + "CTRL-SHIFT-", + "SHIFT-" + bit10 + "-", + bit10 + "-", + "SHIFT-", + "CTRL-" + }; + int[] offsets = + { + 0b1110_0000_0000, // 3584 + 0b1100_0000_0000, // 3072 + 0b1010_0000_0000, // 2560 + 0b0110_0000_0000, // 1536 + 0b0100_0000_0000, // 1024 + 0b0010_0000_0000, // 512 + 0b1000_0000_0000 // 2048 + }; + + for (int i = 0; i < prefixes.length; i++) + { + String prefix = prefixes[i]; + if (label.startsWith(prefix)) { - // this is a candidate; check prefix validity - String prefix = label.substring(0, l - basicKeys[i].length()); - if (prefix.equals("CTRL-")) - return i + 0b1000_0000_0000; // 2048 - if (prefix.equals("SHIFT-")) - return i + 0b0010_0000_0000; // 512 - if (prefix.equals(bit10 + "-")) - return i + 0b0100_0000_0000; // 1024 - if (prefix.equals("SHIFT-" + bit10)) - return i + 0b0110_0000_0000; // 1536 - if (prefix.equals("CTRL-SHIFT-")) - return i + 0b1010_0000_0000; // 2560 - if (prefix.equals("CTRL-" + bit10)) - return i + 0b1100_0000_0000; // 3072 - if (prefix.equals("CTRL-SHIFT-" + bit10)) - return i + 0b1110_0000_0000; // 3584 + String suffix = label.substring(prefix.length()); + Integer idx = basicKeyCodes.get(suffix); + if (idx != null) + { + return idx + offsets[i]; + } } } @@ -2300,6 +2418,24 @@ } /** + * Initialize the {@link #basicKeyCodes} map from the {@link #basicKeys} arrays. + *

+ * WARNING: this is called multiple times during {@link #init()} (and for the subclasses), as the + * {@link #basicKeys} array is initialized in phases. After each phase, {@link #findKey(String)} or + * {@link #findKeyEnd(String)} can be used and the {@link #basicKeyCodes} map must reflect the correct + * state of {@link #basicKeys}. + */ + protected void initBasicKeyCodes() + { + basicKeyCodes.clear(); + + for (int i = 0; i < basicKeys.length; i++) + { + basicKeyCodes.putIfAbsent(basicKeys[i], i); + } + } + + /** * Simple container that stores and returns a context-local instance of * the outer class. */ === modified file 'src/com/goldencode/p2j/ui/LiteralWidget.java' --- src/com/goldencode/p2j/ui/LiteralWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/LiteralWidget.java 2021-01-23 18:01:03 +0000 @@ -2,7 +2,7 @@ ** Module : LiteralWidget.java ** Abstract : server-side literal widget representation. ** -** Copyright (c) 2013-2020, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --------------------------------Description----------------------------------- ** 001 CA 20130927 Created initial version. @@ -11,7 +11,7 @@ ** implementation for the same widget ID. Refactored the widget configuration ** classes: all fields were made public; these classes need to be as dumb as ** possible, all logic related to setting/getting a certain field should be at -** the widget or at the caller. Refs #2254 +** the widget or at the calleribute.er. Refs #2254 ** 004 CA 20141220 Track if an explicit VIEW-AS TEXT phrase is in use. GUI has code dependent on ** this fact. ** 005 CA 20150222 Fixed label's COLUMN and WIDTH-CHARS attributes (in ChUI). @@ -23,6 +23,11 @@ ** 011 CA 20190301 Literals used by side-labels don't have the autoResize config. ** 012 EVL 20200908 Fix for literal widget custom name issue. It can return the valid name if was explicitly ** assigned. +** 013 EVL 20201022 Optimized attributes flush implementation. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** CA 20201202 LITERAL:LABEL is not a queryable attribute. +** CA 20210121 LITERAL:LABEL is a not a settable attribute, unless this is an explicit LITERAL widget +** emitted for a frame (and not a side-label). */ /* ** This program is free software: you can redistribute it and/or modify @@ -91,24 +96,15 @@ */ @LegacyResource(resource = LegacyResource.LITERAL) public class LiteralWidget -extends ControlTextWidget +extends TextBasedWidget { /** - * Side label discriminator, when true this instance represents a side label. - */ - private boolean sideLabel = false; - - /** The associated side widget for this label (when {@link #sideLabel} is true). */ - private GenericWidget sideWidget = null; - - /** * Default constructor. To create a side label, instead use * {@link #createSideLabel(GenericWidget)}. */ public LiteralWidget() { - super(false, new TextConfig()); - setStatic(true); + super(); } /** @@ -122,9 +118,7 @@ public static LiteralWidget createSideLabel(GenericWidget sideWidget) { LiteralWidget result = new LiteralWidget(); - result.sideLabel = true; - result.sideWidget = sideWidget; - + result.makeSideLabel(sideWidget); return result; } @@ -151,140 +145,6 @@ } /** - * Obtains the next sibling widget in the current field group. - * - * @return The next sibling or an unknown handle if this is the only widget or if this is the - * last widget. - */ - @Override - public handle getNextSibling() - { - if (sideLabel && group instanceof FieldGroup) - { - // find the related widget - for (GenericWidget w : ((FieldGroup) group).getWidgets()) - { - if (!(w.config instanceof ControlConfig)) - { - continue; - } - - if (config.id.asInt() == ((ControlConfig) w.config).sideLabelId) - { - return new handle(w); - } - } - } - - return super.getNextSibling(); - } - - /** - * Obtains the previous sibling widget in the current field group. - * - * @return The previous sibling or an unknown handle if this only widget or if this is the - * first widget. - */ - @Override - public handle getPrevSibling() - { - if (sideLabel && group instanceof FieldGroup) - { - // find the related widget - for (GenericWidget w : ((FieldGroup) group).getWidgets()) - { - if (!(w.config instanceof ControlConfig)) - { - continue; - } - - if (config.id.asInt() == ((ControlConfig) w.config).sideLabelId) - { - return new handle(((FieldGroup) group).getPrevChild(w)); - } - } - } - - return super.getPrevSibling(); - } - - /** - * Get the label text for this widget. - *

- * Unlike other widget attribute getters, this API will raise an ERROR condition. - * - * @return The empty string. - */ - @Override - public character getLabel() - { - notQueryable("LABEL", false); - return new character(""); - } - - /** - * This API is a no-op - it just raises an ERROR condition. - * - * @param label - * The new label text. - */ - @Override - public void setLabel(String label) - { - if (frame == null || !frame.isInsideSetup()) - { - notSettable("LABEL", false); - } - else - { - super.setLabel(label); - } - } - - /** - * Obtain the current value in the screen buffer of the backing data for - * this widget and return it as a character type. If - * there is no value in the screen buffer (if it is null - * which is the representation for an uninitialized value), then return - * the empty string. - * - * @return The value as a character type or the empty - * string if the screen buffer value is uninitialized. - */ - @Override - public character getScreenValue() - { - if (!sideLabel) - { - return super.getScreenValue(); - } - - return sideWidget.getLabel(); - } - - /** - * Set the current value in the screen buffer of the backing data for - * this widget. If the given value is null then this - * widget will be set to the uninitialized value. - * - * @param value - * The new value for the widget, use null to set - * the value as uninitialized. - */ - @Override - public void setScreenValue(character value) - { - if (!sideLabel) - { - super.setScreenValue(value); - } - else - { - sideWidget.setLabel(value); - } - } - - /** * Get the name of this LITERAL widget - always unknown value. * * @return Always unknown value. @@ -297,136 +157,35 @@ } /** - * Sets the row or column position for this widget. - *

- * Sub-classes can override this method; at this point, the parameters are valid. - * - * @param isUnknown - * If true the attribute value being set is an unknown value. - * @param value - * The 1-based row or column number. - * @param row - * If true value represents a row number, otherwise - * a column number. - */ - @Override - protected void setColumnOrRowWorker(boolean isUnknown, double value, boolean row) - { - int align = config.align; - - super.setColumnOrRowWorker(isUnknown, value, row); - - // preserve alignment when changing side label position - if (sideLabel) - { - config.align = align; - } - - if (row) - { - // nothing to do for rows - return; - } - - updateSideLabelColumn(value); - } - - /** - * Sets the x or y pixel position for this widget. - *

- * Sub-classes can override this method; at this point, the parameters are - * valid. - * - * @param isUnknown - * If - * true the attribute value being set is an unknown value. - * @param value - * The x or y pixel value. - * @param x - * If true value represents an x - * position, otherwise y. - */ - @Override - protected void setXOrYWorker(boolean isUnknown, int value, boolean x) - { - int align = config.align; - - super.setXOrYWorker(isUnknown, value, x); - - // preserve alignment when changing side label position - if (sideLabel) - { - config.align = align; - } - - if (!x) - { - // nothing to do for y pixel position change - return; - } - - // use the converted column value set in the super implementation - updateSideLabelColumn(config.column); - } - - /** - * Collect the size-related attributes which can be assigned by this widget. - * - * @param names - * The collection where to add the field names. - * @param vals - * The collection where to add the field values. - */ - @Override - protected void getSizeAttrs(List names, List vals) - { - if (sideLabel) - { - names.addAll(Arrays.asList("fixedWidth", "fixedHeight", - "heightChars", "widthChars", - "heightPixels", "widthPixels", - "clientHeightChars", "clientWidthChars", - "clientHeightPixels", "clientWidthPixels")); - vals.addAll(Arrays.asList(config.fixedWidth, config.fixedHeight, - config.heightChars, config.widthChars, - config.heightPixels, config.widthPixels, - config.clientHeightChars, config.clientWidthChars, - config.clientHeightPixels, config.clientWidthPixels)); + * A custom getter for the LABEL attribute. In case of LITERAL widgets, this attribute is not queryable. + * + * @return Always empty string. + */ + @Override + protected character getLabelWorker() + { + notQueryable("LABEL", false); + return new character(""); + } + + /** + * A custom setter for the LABEL attribute. In case of LITERAL widgets, this attribute is not settable. + * + * @param value + * The label. + */ + @Override + protected void setLabelWorker(String value) + { + if (sideLabel) + { + notSettable("LABEL", false); } else { - super.getSizeAttrs(names, vals); - } - } - - /** - * The method updates side label metrics after a change of column - * position. If this widget doesn't represent a side label, this - * method is a no-op. - * - * @param col - * New widget column position. - */ - private void updateSideLabelColumn(double col) - { - if (!sideLabel) - { - return; - } - - String name = getAttr("name", () -> config.name); - - // if the literal is not yet realized, compute its width - if (!getAttr("realized", () -> config.realized) && name != null && getAttr("align", () -> config.align) == ControlEntity.ALIGN_RIGHT) - { - if (name.length() > col - 1) - { - config.column = 0; - config.widthChars = col - 1; - } - else - { - config.widthChars = name.length() + 1; + if (validateLabelAssignment()) + { + setLabelInt(value); } } } === modified file 'src/com/goldencode/p2j/ui/LogicalTerminal.java' --- src/com/goldencode/p2j/ui/LogicalTerminal.java 2020-10-03 16:02:03 +0000 +++ src/com/goldencode/p2j/ui/LogicalTerminal.java 2021-01-26 19:09:30 +0000 @@ -2,7 +2,7 @@ ** Module : LogicalTerminal.java ** Abstract : server runtime implementation of Progress UI concepts ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------------Description----------------------------------- ** 001 NVS 20051026 @23493 Created initial version. @@ -905,6 +905,16 @@ ** registry is modified - this allows to reuse the EventList instance. ** Delay the creation of the ScopeProcessor collections, until they are used. This ** will improve the performance of blocks which do not use frames. +** CA 20201011 Replaced integer sets with a fast access bitmap. +** Improved getRowDisplayEvents. +** HC 20201013 Server-pushed config field changes are serialized with field ids instead of +** names. +** CA 20201015 Do not invalidate the cache if there were no events in the current scope. +** CA 20201112 Avoid pushing the entire frame definition when setting the FRAME or PARENT +** attribute. +** CA 20201117 Added API to delete a widget at runtime. +** SVL 20210112 Added disable(frame, int[], boolean). +** EVL 20210126 Considering -1 event code as no-op for apply(); */ /* @@ -968,9 +978,12 @@ import java.util.function.*; import java.util.logging.*; +import org.roaringbitmap.*; + import com.goldencode.p2j.comauto.*; import com.goldencode.p2j.directory.*; import com.goldencode.p2j.extension.*; +import com.goldencode.p2j.jmx.*; import com.goldencode.p2j.main.*; import com.goldencode.p2j.net.*; import com.goldencode.p2j.net.Session; @@ -1033,7 +1046,15 @@ /** logger */ private static final Logger LOG = LogHelper.getLogger(LogicalTerminal.class.getName()); - + + /** Widget attributes flushes counter */ + private static final SimpleCounter SIMPLE_COUNTER = + SimpleCounter.getInstance(FwdJMX.Counter.WidgetAttrFlushes); + + /** Widget attributes enqueues counter */ + private static final ValuesCounter VALUES_COUNTER = + ValuesCounter.getInstance(FwdJMX.MapCounter.WidgetAttrEnqueues); + /** Context-local instance of LogicalTerminal */ private static final ContextLocal instance = new ContextLocal() @@ -1139,10 +1160,10 @@ private Map frameRegistry = new HashMap<>(); /** Set of dead frames since the last client-side trip. Used by {@link #getChanges()}. */ - private Set deadFrameIds = new HashSet<>(); + private RoaringBitmap deadFrameIds = new RoaringBitmap(); /** Registry of the currently editable frames to get the screen buffers to operate. */ - private Map> editableFrameRegistry = new HashMap<>(); + private Map editableFrameRegistry = new HashMap<>(); /** Registry for all menus for this user's session. */ private Map> menuRegistry = new HashMap<>(); @@ -1167,7 +1188,7 @@ private ScopedDictionary scope = new ScopedDictionary(new LinkedList()); /** A set containing the IDs of all ON .. PERSISTENT RUN triggers */ - private Set persistentTriggers = new HashSet<>(); + private RoaringBitmap persistentTriggers = new RoaringBitmap(); /** CURRENT-WINDOW widget */ private WindowWidget currentWindow = null; @@ -1224,7 +1245,7 @@ * this collection is discarded without other processing, the menus will be cleaned up * naturally, at the end of the scope. */ - private Set pendingMenus = new HashSet<>(); + private RoaringBitmap pendingMenus = new RoaringBitmap(); /** The code of last event function (LAST-EVENT:FUNCTION) */ private int functionKey; @@ -7411,7 +7432,14 @@ return; } - apply(h, eventCode.getValue()); + long evtCodeLong = eventCode.getValue(); + if (evtCodeLong == -1L) + { + // this is a no-op too in case of code is -1 + return; + } + + apply(h, evtCodeLong); } /** @@ -7471,7 +7499,14 @@ return; } - apply(res, eventCode.getValue()); + long evtCodeLong = eventCode.getValue(); + if (evtCodeLong == -1L) + { + // this is a no-op too in case of code is -1 + return; + } + + apply(res, evtCodeLong); } /** @@ -8892,14 +8927,14 @@ { return locate().client.searchInEditor(id, pattern, mode); } - + /** * Disables input for all specified field-level widgets of the frame. *

* This method corresponds to these Progress statements:
* DISABLE ALL
* DISABLE ALL EXCEPT - * + * * @param frame * instance of GenericFrame this request came for * @param widgetId @@ -8908,12 +8943,36 @@ */ public static void disable(GenericFrame frame, int[] widgetId) { + disable(frame, widgetId, true); + } + + /** + * Disables input for all specified field-level widgets of the frame. + *

+ * This method corresponds to these Progress statements:
+ * DISABLE ALL
+ * DISABLE ALL EXCEPT + * + * @param frame + * instance of GenericFrame this request came for + * @param widgetId + * array of field level widget IDs to be disabled or + * null, which means all of them. + * @param resetEntered + * If true then ENTERED flag is reset for the + * widgets which are being disabled. + */ + public static void disable(GenericFrame frame, int[] widgetId, boolean resetEntered) + { // locate the Logical Terminal for this session LogicalTerminal lt = locate(); - - // reset ENTERED flag for widgets which are explicitly disabled; no need to dynamically - // update headers and other content since we are calling getFrameBuffer() below - frame.getFrameBufferRaw().resetEntered(widgetId); + + if (resetEntered) + { + // reset ENTERED flag for widgets which are explicitly disabled; no need to dynamically + // update headers and other content since we are calling getFrameBuffer() below + frame.getFrameBufferRaw().resetEntered(widgetId); + } // refresh editable frames list lt.removeEditableFrameForWidgets(frame, widgetId); @@ -9679,6 +9738,72 @@ } /** + * Delete and detach a widget from a frame, at runtime. The widget can be a dynamic widget or a full + * static frame. + * + * @param frame + * The parent frame. + * @param widget + * The widget. + */ + public static void deleteDynamicWidget(GenericFrame frame, GenericWidget widget) + { + if (widget instanceof BrowseWidget || + widget instanceof BrowseColumnWidget || + widget instanceof TreeWidgetBase) + { + // this has some special behavior, as the browse is made from more than one widget... + frame.pushScreenDefinition(true); + return; + } + + LogicalTerminal lt = locate(); + + lt.client.deleteDynamicWidget(frame.asWidget().getId(), widget.getId()); + } + + /** + * Attach a widget to a frame, at runtime. The widget can be a dynamic widget or a full static frame. + * + * @param frame + * The parent frame. + * @param widget + * The widget to attach. + */ + public static void attachRuntimeWidget(GenericFrame frame, GenericWidget widget) + { + if (widget instanceof BrowseWidget || + widget instanceof BrowseColumnWidget || + widget instanceof TreeWidgetBase) + { + // this has some special behavior, as the browse is made from more than one widget... + frame.pushScreenDefinition(true); + return; + } + + if (widget._dynamic()) + { + WidgetConfig cfg = widget.config(); + ConfigManager mgr = ConfigManager.getInstance(); + WidgetConfig active = mgr.getActiveConfig(cfg.id); + if (active == null) + { + mgr.addWidgetConfig(cfg); + } + else + { + assert(cfg.getClass().equals(active.getClass())); + } + + widget.finishConfigProcessing(); + } + + LogicalTerminal lt = locate(); + + lt.client.attachRuntimeWidget(frame.asWidget().getId(), widget.getId(), widget.config()); + } + + /** * Pushes the screen definition instance down to the client. * * @param frameDef @@ -9997,7 +10122,7 @@ return; } - resourceId = (resourceId == null ? -1 : resourceId); + resourceId = (resourceId == null ? ResourceIdHelper.INVALID_RESOURCE : resourceId); // invalidate the cache lt.cachedEventList = null; @@ -10158,12 +10283,11 @@ TriggerManager.register(lt.scope, events, tid); - Map list = events.getRowDisplayEvents(); + int[][] list = events.getRowDisplayEvents(); if (list != null) { - int[][] map = Utils.integerMapToPrimitive(list); // if there is at least one ROW-DISPLAY event, send it to the client - lt.client.registerRowDisplayEvents(map[0], map[1]); + lt.client.registerRowDisplayEvents(list[0], list[1]); } } @@ -11111,12 +11235,12 @@ synchronized (registryLock) { // getting the ID set from frame that already in the list - Set widIdSet = editableFrameRegistry.get(widFrame); + RoaringBitmap widIdSet = editableFrameRegistry.get(widFrame); // new frame to add if (widIdSet == null) { - widIdSet = new HashSet(); + widIdSet = new RoaringBitmap(); editableFrameRegistry.put(widFrame, widIdSet); } @@ -11149,7 +11273,7 @@ synchronized (registryLock) { // check if the frame is in the list - Set widIdSet = editableFrameRegistry.get(widFrame); + RoaringBitmap widIdSet = editableFrameRegistry.get(widFrame); // if none - do nothing if (widIdSet != null) { @@ -11298,7 +11422,7 @@ } /** - * Internal worker for {@link #applyChanges(Serializable)}. + * Internal worker for {@link #applyChanges(Externalizable)}. * * @param frameId * The ID of frame which should be removed from registry. @@ -11569,7 +11693,7 @@ * @return The widget which corresponds to the given widget ID, which may * be a frame, or null if the widget ID is invalid. */ - private GenericWidget getWidgetForIdInt(int widgetId) + protected GenericWidget getWidgetForIdInt(int widgetId) { synchronized (registryLock) { @@ -12900,10 +13024,11 @@ locate().nesting--; } - // invalidate the cache - cachedEventList = null; - - TriggerManager.scopeFinished(scope); + if (TriggerManager.scopeFinished(scope)) + { + // invalidate the cache + cachedEventList = null; + } } } @@ -12982,13 +13107,7 @@ if (!deadFrames.isEmpty()) { - sstate.deadFrames = new int[deadFrames.size()]; - Iterator fitr = deadFrames.iterator(); - int idx = 0; - while (fitr.hasNext()) - { - sstate.deadFrames[idx++] = fitr.next(); - } + sstate.deadFrames = Utils.integerCollectionToPrimitive(deadFrames); } } @@ -14568,7 +14687,7 @@ private ArrayList hidden = null; /** Viewed in current iteration frames. */ - private HashSet viewed = null; + private RoaringBitmap viewed = null; /** Track frame hiding in current scope. */ private boolean canHide = false; @@ -15164,9 +15283,9 @@ * * @return See above. */ - private HashSet getViewed() + private RoaringBitmap getViewed() { - return viewed == null ? (viewed = new HashSet<>()) : viewed; + return viewed == null ? (viewed = new RoaringBitmap()) : viewed; } /** @@ -17318,13 +17437,17 @@ * Widget id. * @param fname * Field name. + * @param fid + * Field id. * @param value * New value. */ - public void enqueuWidgetAttr(int widgetId, String fname, Object value) + public void enqueuWidgetAttr(int widgetId, String fname, int fid, Object value) { + VALUES_COUNTER.update(fname, 1); + widgetAttrQueue.add(widgetId); - widgetAttrQueue.add(fname); + widgetAttrQueue.add(fid); widgetAttrQueue.add(value); } @@ -17339,6 +17462,9 @@ return; } + SIMPLE_COUNTER.update(1); + + // this will trigger getChanges, the actual flush will happen there client.flushEnqueuedWidgetAttrs(); } } === modified file 'src/com/goldencode/p2j/ui/MenuContainerWidget.java' --- src/com/goldencode/p2j/ui/MenuContainerWidget.java 2020-10-07 18:32:15 +0000 +++ src/com/goldencode/p2j/ui/MenuContainerWidget.java 2020-11-17 20:03:34 +0000 @@ -30,6 +30,10 @@ ** 015 HC 20200313 Javadoc fixes. ** 016 HC 20200926 Performance optimizations of server-client config state synchronization. ** AIL 20201007 Override canPushWidgetAttr method in order to define custom constraints. +** EVL 20201019 Added specific version of getId() for client driven attribute. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. */ /* ** This program is free software: you can redistribute it and/or modify @@ -379,6 +383,19 @@ } /** + * Gets the numeric ID of this widget (this is used as an index into the + * screen-buffer among other things). + * + * @return The numeric ID of this widget. + */ + @Override + public int getId() + { + // client driven ID getter case + return getAttr("id", () -> (int)(config.id != null ? config.id.asInt() : -1), true); + } + + /** * Core initialization processing. * * @param defClass @@ -526,7 +543,7 @@ { mgr = ConfigManager.getInstance(); } - WidgetConfig active = mgr.getActiveConfig(getAttr("id", () -> config.id)); + WidgetConfig active = mgr.getActiveConfig(getAttr("id", () -> config.id, true)); if (active == null) { mgr.addWidgetConfig(config); @@ -635,7 +652,27 @@ // shared menu can't be deleted until all are out of scope. return false; } + + return true; + } + + /** + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + super.delete(); + // let the root delete the entire sub-tree - this is needed to avoid client-side trips // for each element in this sub-tree. MenuWidget root = (MenuWidget) findRootMenu(this); @@ -644,9 +681,7 @@ deleted = true; LogicalTerminal.deregisterMenu(getId()); - ConfigManager.getInstance().removeWidgetConfig(getAttr("id", () -> config.id)); - - return true; + ConfigManager.getInstance().removeWidgetConfig(getAttr("id", () -> config.id, true)); } /** @@ -748,7 +783,7 @@ */ private void pushMenuDescriptionImpl(boolean force) { - int menuId = getAttr("id", () -> config.id).asInt(); + int menuId = getId(); menuDef = new MenuDescription(menuId); if (this.getParent() == null) { @@ -766,7 +801,7 @@ // we are pushing, disable batch for all sub-menu's sm.batch = false; sm.pushMenuDescriptionImpl(force); - menuDef.addChildDescription(sm.menuDef, sm.getAttr("id", () -> sm.config.id).asInt()); + menuDef.addChildDescription(sm.menuDef, sm.getId()); } } === modified file 'src/com/goldencode/p2j/ui/MenuItemWidget.java' --- src/com/goldencode/p2j/ui/MenuItemWidget.java 2020-10-07 18:32:15 +0000 +++ src/com/goldencode/p2j/ui/MenuItemWidget.java 2021-01-15 20:55:46 +0000 @@ -33,6 +33,13 @@ ** 018 HC 20200313 Javadoc fixes. ** 019 HC 20200926 Performance optimizations of server-client config state synchronization. ** AIL 20201007 Override canPushWidgetAttr method in order to define custom constraints. +** HC 20201010 Implemented selective config flushing. +** EVL 20201019 Added specific version of getId() for client driven attribute. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201028 The SUBTYPE can be set if the owner is not realized. +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. */ /* @@ -158,8 +165,9 @@ */ public integer getMnemonic() { - return getAttr("mnemonic", (Supplier)() -> config.mnemonic) == null ? new integer() : - new integer(getAttr("mnemonic", (Supplier)() -> config.mnemonic)); + return getAttr("mnemonic", + (Supplier)() -> (config.mnemonic == null ? new integer() + : new integer(config.mnemonic)), true); } /** @@ -170,9 +178,8 @@ */ public character getPreprocessedLabel() { - return getAttr("preprocessedLabel ", () -> config.preprocessedLabel )== null - ? new character() - : new character(getAttr("preprocessedLabel", () -> config.preprocessedLabel)); + return getAttr("preprocessedLabel", () -> (config.preprocessedLabel == null ? new character() : + new character(config.preprocessedLabel)), true); } /** @@ -253,6 +260,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) @Override public void setReadOnly(logical r) { @@ -268,6 +276,7 @@ * @param r * true if the widget should be write-protected. */ + @LegacyAttribute(name = "READ-ONLY", setter = true) public void setReadOnly(boolean r) { if (config.readOnly == r) @@ -457,9 +466,20 @@ if (p._isRealized()) { - String msg = "**Unable to set SUBTYPE because the MENU-ITEM widget has been realised"; - ErrorManager.recordOrShowError(new int[] {4053}, new String[] {msg}, false, false, false); - return; + boolean realized = true; + p = MenuContainerWidget.findRootMenu(p); + if (p != null && p instanceof MenuWidget) + { + GenericWidget owner = ((MenuWidget) p).getOwner(); + realized = owner != null && owner._isRealized(); + } + + if (realized) + { + String msg = "**Unable to set SUBTYPE because the MENU-ITEM widget has been realised"; + ErrorManager.recordOrShowError(new int[] {4053}, new String[] {msg}, false, false, false); + return; + } } } @@ -486,6 +506,19 @@ } /** + * Gets the numeric ID of this widget (this is used as an index into the + * screen-buffer among other things). + * + * @return The numeric ID of this widget. + */ + @Override + public int getId() + { + // client driven ID getter case + return getAttr("id", () -> (int)(config.id != null ? config.id.asInt() : -1), true); + } + + /** * Gets the CHECKED writable attribute. * * @return The current value of the CHECKED attribute. @@ -597,15 +630,33 @@ return false; } + return true; + } + + /** + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + super.delete(); + MenuContainerWidget root = MenuContainerWidget.findRootMenu(this); ((MenuWidget) root).detachMenu(this); deleted = true; LogicalTerminal.deregisterMenu(getId()); - ConfigManager.getInstance().removeWidgetConfig(getAttr("id", () -> config.id)); - - return true; + ConfigManager.getInstance().removeWidgetConfig(getAttr("id", () -> config.id, true)); } /** === modified file 'src/com/goldencode/p2j/ui/MenuWidget.java' --- src/com/goldencode/p2j/ui/MenuWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/MenuWidget.java 2020-10-22 14:25:52 +0000 @@ -26,6 +26,8 @@ ** GES 20200306 Shifted to TitledElement. ** RFB 20200330 Full implemented the TitledElement methods. ** RFB 20200331 Post-Code Review cleanup in comments. +** 014 HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. */ /* @@ -111,7 +113,7 @@ /** Private storage for TITLE-FONT: honored, but not acted upon by the client */ private int64 titleFont = null; - + /** * Default constructor. * === added file 'src/com/goldencode/p2j/ui/MinHeightCharsInterface.java' --- src/com/goldencode/p2j/ui/MinHeightCharsInterface.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/ui/MinHeightCharsInterface.java 2020-12-25 16:37:53 +0000 @@ -0,0 +1,88 @@ +/* + ** Module : MinHeightCharsInterface.java + ** Abstract : Groups widgets that have MIN-HEIGHT-CHARS attribute. + ** + ** Copyright (c) 2020, Golden Code Development Corporation. + ** + ** -#- -I- --Date-- ----------------------------Description-------------------------------------- + ** 001 SVL 20201221 Created initial version. + */ +/* + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU Affero General Public License as + ** published by the Free Software Foundation, either version 3 of the + ** License, or (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU Affero General Public License for more details. + ** + ** You may find a copy of the GNU Affero GPL version 3 at the following + ** location: https://www.gnu.org/licenses/agpl-3.0.en.html + ** + ** Additional terms under GNU Affero GPL version 3 section 7: + ** + ** Under Section 7 of the GNU Affero GPL version 3, the following additional + ** terms apply to the works covered under the License. These additional terms + ** are non-permissive additional terms allowed under Section 7 of the GNU + ** Affero GPL version 3 and may not be removed by you. + ** + ** 0. Attribution Requirement. + ** + ** You must preserve all legal notices or author attributions in the covered + ** work or Appropriate Legal Notices displayed by works containing the covered + ** work. You may not remove from the covered work any author or developer + ** credit already included within the covered work. + ** + ** 1. No License To Use Trademarks. + ** + ** This license does not grant any license or rights to use the trademarks + ** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks + ** of Golden Code Development Corporation. You are not authorized to use the + ** name Golden Code, FWD, or the names of any author or contributor, for + ** publicity purposes without written authorization. + ** + ** 2. No Misrepresentation of Affiliation. + ** + ** You may not represent yourself as Golden Code Development Corporation or FWD. + ** + ** You may not represent yourself for publicity purposes as associated with + ** Golden Code Development Corporation, FWD, or any author or contributor to + ** the covered work, without written authorization. + ** + ** 3. No Misrepresentation of Source or Origin. + ** + ** You may not represent the covered work as solely your work. All modified + ** versions of the covered work must be marked in a reasonable way to make it + ** clear that the modified work is not originating from Golden Code Development + ** Corporation or FWD. All modified versions must contain the notices of + ** attribution required in this license. + */ + +package com.goldencode.p2j.ui; + +import com.goldencode.p2j.util.*; + +/** + * Groups widgets that have MIN-HEIGHT-CHARS attribute. + */ +public interface MinHeightCharsInterface +{ + /** + * Implements the MIN-HEIGHT-CHARS attribute getter. + * + * @return minimum height of the widget, in character units. + */ + @LegacyAttribute(name = "MIN-HEIGHT-CHARS") + public decimal getMinHeightChars(); + + /** + * Sets the MIN-HEIGHT-CHARS writable attribute. + * + * @param min + * The new value for the MIN-HEIGHT-CHARS attribute. + */ + @LegacyAttribute(name = "MIN-HEIGHT-CHARS", setter = true) + public void setMinHeightChars(NumberType min); +} \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/PaneEntity.java' --- src/com/goldencode/p2j/ui/PaneEntity.java 2020-10-04 18:23:10 +0000 +++ src/com/goldencode/p2j/ui/PaneEntity.java 2020-10-22 14:25:52 +0000 @@ -29,6 +29,8 @@ ** HC 20191126 Implemented RESIZE attribute for DIALOG-BOX. This is an extension to 4GL. ** 014 HC 20200313 Javadoc fixes. ** 015 HC 20201004 Performance optimizations of server-client config state synchronization. +** HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. */ /* ** This program is free software: you can redistribute it and/or modify @@ -178,7 +180,7 @@ @Override public void setThreeD(boolean threeD) { - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { ErrorManager.recordOrShowError(4053, String.format( @@ -212,7 +214,7 @@ } else { - return new decimal(getAttr("virtualHeightChars", () -> config.virtualHeightChars)); + return new decimal(getAttr("virtualHeightChars", () -> config.virtualHeightChars, true)); } } @@ -244,7 +246,7 @@ } else { - return new decimal(getAttr("virtualWidthChars", () -> config.virtualWidthChars)); + return new decimal(getAttr("virtualWidthChars", () -> config.virtualWidthChars, true)); } } @@ -276,7 +278,7 @@ } else { - return new integer(getAttr("virtualHeightPixels", () -> config.virtualHeightPixels)); + return new integer(getAttr("virtualHeightPixels", () -> config.virtualHeightPixels, true)); } } @@ -308,7 +310,7 @@ } else { - return new integer(getAttr("virtualWidthPixels", () -> config.virtualWidthPixels)); + return new integer(getAttr("virtualWidthPixels", () -> config.virtualWidthPixels, true)); } } === modified file 'src/com/goldencode/p2j/ui/ProgressBarWidget.java' --- src/com/goldencode/p2j/ui/ProgressBarWidget.java 2020-10-07 11:19:20 +0000 +++ src/com/goldencode/p2j/ui/ProgressBarWidget.java 2020-10-22 14:25:52 +0000 @@ -13,6 +13,7 @@ ** resolves compilation issues when converting ProgressBar OCX with literal ** arguments. ** 006 SBI 20200810 Removed seValue(double)-the compilation issue when converting ProgressBar. +** 007 EVL 20201022 Optimized attributes flush implementation. */ /* @@ -122,7 +123,7 @@ @Override public logical isEnabled() { - return new logical(getAttr("enabled", () -> config.enabled)); + return new logical(getAttr("enabled", () -> config.enabled, true)); } /** @@ -134,7 +135,7 @@ @Override public void setEnabled(boolean value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { if (config.enabled == value) { @@ -457,7 +458,7 @@ return; } - if (value < 0.0 || value < getAttr("min ", () -> config.min )|| value > getAttr("max", () -> config.max)) + if (value < 0.0 || value < getAttr("min", () -> config.min )|| value > getAttr("max", () -> config.max)) { ErrorManager.recordOrShowError(5990, String.format( "Error occured while accessing component property/method: Value\n" === modified file 'src/com/goldencode/p2j/ui/RadioSetWidget.java' --- src/com/goldencode/p2j/ui/RadioSetWidget.java 2020-09-28 10:09:11 +0000 +++ src/com/goldencode/p2j/ui/RadioSetWidget.java 2021-01-29 09:30:09 +0000 @@ -2,7 +2,7 @@ ** Module : RadioSetWidget.java ** Abstract : server side radio-set widget implementation ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 NVS 20051026 @23494 Created initial version. @@ -48,6 +48,12 @@ ** 043 EVL 20190806 Fixed the vertical size calculation in certain conditions. ** 044 EVL 20191211 Fix for NPE with usage of the config.pairs value. ** 045 EVL 20200225 Fix for config.pairs issue when adding items with proper data value. +** 046 EVL 20201022 Optimized attributes flush implementation. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal +** usage, within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** CA 20210123 Changing the FORMAT must set this at the radio-set's items, too. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. */ /* ** This program is free software: you can redistribute it and/or modify @@ -163,7 +169,7 @@ @Override public void setHorizontal(boolean value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { config.horizontal = value; resetSize(config.items); @@ -259,7 +265,7 @@ @Override public void setExpand(boolean value) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { if (config.expand == value) { @@ -465,6 +471,52 @@ } /** + * Sets the format specification. + * + * @param format + * The format specification. + */ + @LegacyAttribute(name = "FORMAT", setter = true, ignore = true) + @Override + public void setFormat(String format) + { + if (!config.dynamic && (frame == null || frame.isInsideSetup())) + { + return; + } + + notSettable("FORM", true); + } + + /** + * Gets the format specification. + * + * @return The format specification. + */ + @Override + @LegacyAttribute(name = "FORMAT", ignore = true) + public character getFormat() + { + if (!config.dynamic && (frame == null || frame.isInsideSetup())) + { + return new character(); + } + + notQueryable("FORM", true); + return new character(); + } + + /** + * Checks if the given widget has valid format string or even has the format attribute. + * + * @return always FALSE. + */ + public boolean hasFormat() + { + return false; + } + + /** * Implements the ADD-FIRST() widget method, which adds a single label * and value pair to the beginning of the list. * @@ -1257,7 +1309,7 @@ * The new value for the widget, use null to set the value as uninitialized. */ @Override - public void setScreenValue(character value) + protected void setScreenValueInt(character value) { if (value != null && "".equals(value.toStringMessage())) { @@ -1265,7 +1317,7 @@ return; } - super.setScreenValue(value); + super.setScreenValueInt(value); } /** === modified file 'src/com/goldencode/p2j/ui/ScreenBuffer.java' --- src/com/goldencode/p2j/ui/ScreenBuffer.java 2020-10-08 19:08:27 +0000 +++ src/com/goldencode/p2j/ui/ScreenBuffer.java 2020-11-02 23:41:12 +0000 @@ -79,6 +79,16 @@ ** ECF 20200928 Minor optimization. ** IAS 20201006 Get rid of map <-> array conversion in (de)serialization ** GES 20201008 Fixed regression in readExternal(). +** GES 20201016 Complete reworking of internal data structures to use arrays instead of maps. +** GES 20201018 Added contiguous mode for id to index processing, the first added widget will +** be placed at index 0 and the id will be used as an offset value. Subsequent +** adds/lookups will use this offset if subtracting it from the widget id yields +** a valid index position. This eliminates map processing for the common cases. +** GES 20201025 Use sparse arrays to maximize the possible usage of contiguous mode. +** Serialization was changed to compress the arrays during transport. Fixed NPE +** in markAllchanged(). +** GES 20201030 Fix for reallocate() to handle the scenario where the widget ID is in the +** front of the array and the array length is too large to allocate. */ /* ** This program is free software: you can redistribute it and/or modify @@ -135,13 +145,14 @@ package com.goldencode.p2j.ui; +import java.util.function.*; import static com.goldencode.util.NativeTypeSerializer.*; import java.io.*; import java.util.*; import com.goldencode.p2j.ui.client.*; -import com.goldencode.p2j.util.BaseDataType; +import com.goldencode.p2j.util.*; import com.goldencode.util.*; /** @@ -176,27 +187,36 @@ /** State flag */ public static final byte STATE_MAX = UNINITIALIZED + CHANGED + ENTERED; + + /** Maximum number of elements to allocate for a sparse array. */ + private final int MAX_SPARSE_ARRAY_SIZE = 512; + + /** Minimum number of elements to allocate when growing the array. */ + private final int MIN_ELEMENTS = 16; - /** Frame ID where this screen buffer belongs */ + /** Frame ID where this screen buffer belongs. */ private int baseId = -1; - /** Maximum number of entries */ - private int size = -1; - - /** - * Per widget state map. This map must not be accessed directly to put, remove or get values. - * Use helper methods for respective operation. - */ - private Map state = new HashMap(); - - /** The map to store (widget ID, current value) pairs */ - private Map buffer = new HashMap(); - - /** The map to store header widget values. */ - private Map headers = new HashMap(); + /** Number of non-header widgets in the buffer. */ + private int size = 0; + + /** Optimized index lookup for contiguous IDs. */ + private int offset = -1; + + /** Mapping of widget ID to array index position. */ + private transient Map indices = new HashMap(); + + /** Mark an "extra-sparse" mode. Don't fail putWidgetValue/setState when a widget id is not found. */ + private transient boolean permissive = false; + + /** Core storage (widget ID, current value, state flags). */ + private StatefulWidgetRecord[] buffer = null; + + /** All header widget values for this frame. */ + private WidgetRecord[] headers = null; /** The map for some screen values. */ - private Map values = new HashMap(); + private Map values = new HashMap<>(); /** Key code which will be transferred in key related events. */ private int keyCode = -1; @@ -243,32 +263,57 @@ private UIStatement currentStatement = null; /** - * The default constructor creates a rudimentary ScreenBuffer which remains - * empty. + * The default constructor creates a rudimentary ScreenBuffer which remains empty. */ public ScreenBuffer() { - size = 0; } /** - * Constructor. - * Prepares internal structures to take the data. - * - * @param frameId - * the ID of the frame where this ScreenBuffer belongs - * @param numWidgets - * the total number of widgets in the frame - */ - public ScreenBuffer(int frameId, int numWidgets) - { - size = numWidgets; - baseId = frameId; - } - - /** - * Constructor. - * Prepares internal structures to take the data. + * Constructor. Prepares internal structures to take data up to the given number of widgets. + *

+ * Avoid using this constructor for any cases that are performance critical. This approach does not + * easily enable all widgets to be stored by offset and often will result in the slower mapping + * approach. The exception is when you know the size of the range (maximum widget ID minus the minimum + * widget ID). In that case the result will work for offset mode. + * + * @param frameId + * The ID of the frame to which this screen buffer belongs. + * @param num + * The total number of widgets expected in the frame. + */ + public ScreenBuffer(int frameId, int num) + { + // make sure we have at a multiple of MIN_ELEMENTS + num = ((num % MIN_ELEMENTS) == 0) ? num : ((num / MIN_ELEMENTS) + 1) * MIN_ELEMENTS; + + buffer = new StatefulWidgetRecord[num]; + baseId = frameId; + permissive = true; + } + + /** + * Constructor which allocates a sparse array buffer (if possible) which will allow all widgets within + * the minimum and maximum ID range to be stored/retrieved via the offset method (avoiding the slow + * map method of storage). + * + * @param frameId + * The ID of the frame to which this screen buffer belongs. + * @param min + * The smallest widget ID to be used for this screen buffer. + * @param max + * The largest widget ID to be used for this screen buffer. + */ + public ScreenBuffer(int frameId, int min, int max) + { + buffer = new StatefulWidgetRecord[calcWidgetArraySize(min, max)]; + offset = min; + baseId = frameId; + permissive = true; + } + + /** + * Constructor. Prepares internal structures to take the data. * * @param frameId * the ID of the frame where this ScreenBuffer belongs @@ -278,28 +323,37 @@ public ScreenBuffer(int frameId, int[] ids) { baseId = frameId; - if (ids != null) + + if (ids != null && ids.length > 0) { - size = ids.length; + int min = ids[0]; + int max = ids[0]; + for (int i = 0; i < ids.length; i++) { - setStateInt(ids[i], UNINITIALIZED); - } - } - else - { - size = 0; + min = Math.min(min, ids[i]); + max = Math.max(max, ids[i]); + } + + offset = min; + buffer = new StatefulWidgetRecord[calcWidgetArraySize(min, max)]; + + for (int j = 0; j < ids.length; j++) + { + int idx = calcNextIndex(ids[j]); + addWidgetInt(ids[j], idx); + } } } /** - * Gets the maximum size of this screen buffer. + * Gets the current number of widgets in this screen buffer. * - * @return The total number of widgets in the frame + * @return The total number of non-header widgets in the frame */ public int size() { - return size; + return (buffer == null) ? 0 : size; } /** @@ -313,18 +367,14 @@ } /** - * Adding new widget into screen buffer for the given widget ID. + * Adds a dynamic widget into the screen buffer. * * @param widgetID * The ID of the widget to be added. */ public void addWidget(int widgetID) { - // store initial state - setStateInt(widgetID, UNINITIALIZED); - - // and increase the size - size++; + addWidgetInt(widgetID, calcNextIndex(widgetID)); } /** @@ -335,34 +385,38 @@ */ public void removeWidget(int widgetID) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) + int idx = idToIndex(widgetID); + + // unknown widget, nothing to do + if (idx == -1) return; - // remove the state entry - state.remove(widgetID); - // remove the value itself - buffer.remove(widgetID); - values.remove(widgetID); - // and decrease the total size - size--; + removeWidgetInt(idx); } /** - * Storing the new value for the given widget ID. + * Store the new value for the given widget ID. + *

+ * In permissive mode, this method will not fail if the widget id is not already known. Instead, the + * widget is added implicitly. This does not occur under the normal (non-permissive) mode. * * @param widgetID * The ID of the widget to which the data is related. * @param value * New value as an Object instance. + * + * @return {@code true} if this was a valid/existing widget. */ - public void putWidgetValue(int widgetID, Object value) + public boolean putWidgetValue(int widgetID, Object value) { - putWidgetValue(widgetID, value, false); + return putWidgetValue(widgetID, value, false); } /** - * Storing the new value for the given widget ID. + * Store the new value for the given widget ID. + *

+ * In permissive mode, this method will not fail if the widget id is not already known. Instead, the + * widget is added implicitly. This does not occur under the normal (non-permissive) mode. * * @param widgetID * The ID of the widget to which the data is related. @@ -372,43 +426,56 @@ * Flag indicating if the widget's state is preserved. * When false, the widget state is set to {@link #CHANGED}. Otherwise * it remains unchanged. + * + * @return {@code true} if this was a valid/existing widget. */ - public void putWidgetValue(int widgetID, Object value, boolean preserveState) + public boolean putWidgetValue(int widgetID, Object value, boolean preserveState) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) - return; + int idx = idToIndex(widgetID); + + // unknown widget, nothing to do + if (idx == -1) + { + if (permissive) + { + idx = calcNextIndex(widgetID); + addWidgetInt(widgetID, idx); + } + else + { + return false; + } + } + // change state, if needed if (!preserveState) { - // if widget already changed state override initial - byte stateNew = getStateInt(widgetID); - - // change state - stateNew &= STATE_MAX - UNINITIALIZED - ENTERED; - stateNew |= CHANGED; - // store new state - setStateInt(widgetID, stateNew); + buffer[idx].state = CHANGED; } // and store new widget value - buffer.put(widgetID, value); + buffer[idx].value = value; + + return true; } /** - * Storing the new header value for the given widget ID. + * Allocate and copy the given header widgets. * - * @param widgetID - * The ID of the widget to which the data is related. - * @param value - * New value as an Object instance. + * @param elements + * The header elements to copy. */ - public void putHeaderValue(int widgetID, Object value) + public void allocateHeaders(FrameElement[] elements) { - if (headers == null) - headers = new LinkedHashMap(); - - headers.put(widgetID, value); + if (elements == null || elements.length == 0) + return; + + headers = new WidgetRecord[elements.length]; + + for (int i = 0; i < elements.length; i++) + { + headers[i] = new WidgetRecord(elements[i].getWidget().getId(), elements[i].get()); + } } /** @@ -416,73 +483,99 @@ * * @param widgetID * The ID of the widget to which the data is related. - * @return State of this widget or -1 if such ID does not - * exist. + * @return State of this widget or -1 if such ID does not exist. */ public byte getState(int widgetID) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) - { + int idx = idToIndex(widgetID); + + // unknown widget, nothing to do + if (idx == -1) return -1; - } - + // get the current status - return getStateInt(widgetID); + return buffer[idx].state; } /** - * Stores the new state for the widget with the given widget ID. - * IF the new state is UNINITIALIZED, the previous value of the widget, - * if any, is deleted. + * Stores the new state for the widget with the given widget ID. If the new state is UNINITIALIZED, + * the previous value of the widget, if any, is deleted. + *

+ * In permissive mode, this method will not fail if the widget id is not already known. Instead, the + * widget is added implicitly. This does not occur under the normal (non-permissive) mode. * * @param widgetID * The ID of the widget to which the data is related. * @param state - * new state + * The new state flags. */ public void setState(int widgetID, byte state) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) + int idx = idToIndex(widgetID); + + // unknown widget, nothing to do + if (idx == -1) { - return; + if (permissive) + { + idx = calcNextIndex(widgetID); + addWidgetInt(widgetID, idx); + } + else + { + return; + } } + // invalid state, nothing to do if (state < STATE_MIN || state > STATE_MAX) return; // remove widget value if become uninitialized if (state == UNINITIALIZED) { - buffer.remove(new Integer(widgetID)); + buffer[idx].value = null; } // store new state value for this widget ID - setStateInt(widgetID, (byte)state); + buffer[idx].state = state; } /** - * Requests the iterator to walk through the content of the screen buffer. + * Remove all widget IDs from the given set if the widget has a non-null value in the screen buffer. * - * @return The iterator for the screen buffer content. + * @param widgets + * The set from which the IDs will be removed. */ - public Iterator getWidgetIDIterator() + public void removeWidgetIDs(Set widgets) { - return buffer.keySet().iterator(); + if (buffer != null) + { + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null && buffer[i].value != null) + { + widgets.remove(buffer[i].id); + } + } + } } /** - * Requests the iterator to walk through the content of the header widgets. + * Executes the given consumer for each header widget. * - * @return The iterator for the screen buffer content. + * @param consume + * The function that consumes the header widget values. */ - public Iterator getHeaderIDIterator() + public void processHeaders(BiConsumer consume) { - if (headers == null) - return (new HashMap()).keySet().iterator(); - - return headers.keySet().iterator(); + if (headers != null) + { + for (int i = 0; i < headers.length; i++) + { + consume.accept(headers[i].id, headers[i].value); + } + } } /** @@ -495,25 +588,15 @@ */ public Object getWidgetValue(int widgetID) { - return buffer.get(widgetID); - } - - /** - * Getting the header value in the Object form for the given widget ID. - * - * @param widgetID - * The numeric ID of a widget whose value must be returned. - * - * @return The Object instance of the widget value. - */ - public Object getHeaderValue(int widgetID) - { - if (headers == null) + int idx = idToIndex(widgetID); + + // unknown widget, nothing to return + if (idx == -1) return null; - return headers.get(widgetID); + return buffer[idx].value; } - + /** * Set screen value for specified widget. * @@ -717,7 +800,7 @@ */ public ScreenBuffer getChanged() { - ScreenBuffer diff = new ScreenBuffer(baseId, size); + ScreenBuffer diff = new ScreenBuffer(baseId, buffer == null ? 0 : buffer.length); // copy down value diff.down = this.down; @@ -735,38 +818,35 @@ // copy current frame value diff.frameValue = this.frameValue; - // copying state info - copyState(diff); - - // copy changed widget values - Iterator iter = buffer.keySet().iterator(); - - Integer id = null; - Object value = null; - - while (iter.hasNext()) + // copy widget state and values + if (buffer != null) { - id = iter.next(); - - // get current state and check for CHANGED flag - if (!((getStateInt(id) & CHANGED) == CHANGED)) + diff.offset = this.offset; + + for (int j = 0; j < buffer.length; j++) { - continue; + if (buffer[j] != null) + { + diff.addWidgetInt(buffer[j].id, j); + diff.buffer[j].state = buffer[j].state; + + // get current state and check for CHANGED flag + if ((buffer[j].state & CHANGED) == CHANGED) + { + diff.buffer[j].value = buffer[j].value; + } + } } - - value = buffer.get(id); - diff.buffer.put(id, value); } - + // copy header widgets if (headers != null) { - iter = headers.keySet().iterator(); + diff.headers = new WidgetRecord[headers.length]; - while(iter.hasNext()) + for (int i = 0; i < headers.length; i++) { - id = iter.next(); - diff.putHeaderValue(id, headers.get(id)); + diff.headers[i] = new WidgetRecord(headers[i]); } } @@ -785,33 +865,38 @@ * one. Changed widgets are marked as ENTERED. * * @param diff - * a differential ScreenBuffer to copy widget values - * from. + * a differential ScreenBuffer to copy widget values from. */ public void mergeChanged(ScreenBuffer diff) { // copy changed widget values - Iterator iter = diff.buffer.keySet().iterator(); - - Integer id = null; - Object value = null; - - while (iter.hasNext()) + if (diff.buffer != null) { - id = iter.next(); + for (int i = 0; i < diff.buffer.length; i++) + { + if (diff.buffer[i] != null) + { + // get current status from diff buffer and check for CHANGED flag + if ((diff.buffer[i].state & CHANGED) == CHANGED) + { + int idx = idToIndex(diff.buffer[i].id); - // get current status from diff buffer and check for CHANGED flag - if (!((diff.getStateInt(id) & CHANGED) == CHANGED)) - continue; - - value = diff.buffer.get(id); - this.buffer.put(id, value); - // get current state from this current buffer - byte stateCurr = getStateInt(id); - // reset UNINITIALIZED - stateCurr &= ~UNINITIALIZED; - stateCurr |= ENTERED; - setStateInt(id, stateCurr); + // if the widget doesn't already exist, we need to add it + if (idx == -1) + { + idx = calcNextIndex(diff.buffer[i].id); + addWidgetInt(diff.buffer[i].id, idx); + } + + buffer[idx].value = diff.buffer[i].value; + buffer[idx].state = diff.buffer[i].state; + + // reset UNINITIALIZED + buffer[idx].state &= ~UNINITIALIZED; + buffer[idx].state |= ENTERED; + } + } + } } // copy the frame line @@ -830,32 +915,37 @@ /** * Resets all widgets having ENTERED state. + * * @param ids * Array of IDs of entered widgets. */ public void resetEntered(int[] ids) { + if (buffer == null) + return; + byte flags = STATE_MAX - ENTERED; + // reset entered state bit for each widget if (ids == null) { - Iterator iter = state.keySet().iterator(); - - while (iter.hasNext()) + for (int j = 0; j < buffer.length; j++) { - Integer id = iter.next(); - // reset entered state bit for next widget - bitwiseAndState(id.intValue(), flags); + if (buffer[j] != null) + { + buffer[j].state &= flags; + } } } else { for (int i = 0; i < ids.length; i++) { - if (isIdValid(ids[i])) + int idx = idToIndex(ids[i]); + + if (idx != -1) { - // reset entered state bit for next widget - bitwiseAndState(ids[i], flags); + buffer[idx].state &= flags; } } } @@ -866,25 +956,45 @@ */ public void markAllChangedStatesAsEntered() { - Iterator iter = state.keySet().iterator(); - - while (iter.hasNext()) - { - Integer id = iter.next(); - - byte stateCurr = getStateInt(id); - - if (!((stateCurr & CHANGED) == CHANGED)) - { - continue; + if (buffer != null) + { + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null && (buffer[i].state & CHANGED) == CHANGED) + { + // reset UNINITIALIZED + buffer[i].state &= ~UNINITIALIZED; + buffer[i].state |= ENTERED; + } + } + } + } + + /** + * Mark all widgets as changed and return an array of all widget IDs. + * + * @return All widget IDs. This will be a 0 sized array (not {@code null}) when there are no + * widgets in the buffer. + */ + public int[] markAllChanged() + { + int[] ids = new int[size]; + + if (buffer != null) + { + int idx = 0; + + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null) + { + ids[idx++] = buffer[i].id; + buffer[i].state = CHANGED; + } } - - // reset UNINITIALIZED - stateCurr &= ~UNINITIALIZED; - stateCurr |= ENTERED; - - setStateInt(id, stateCurr); } + + return ids; } /** @@ -892,14 +1002,17 @@ */ public void resetChanged() { - byte flags = STATE_MAX - CHANGED; - Iterator iter = state.keySet().iterator(); - - while (iter.hasNext()) + if (buffer != null) { - Integer id = iter.next(); - // reset changed state bit for next widget - bitwiseAndState(id.intValue(), flags); + byte flags = STATE_MAX - CHANGED; + + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null) + { + buffer[i].state &= flags; + } + } } } @@ -908,14 +1021,17 @@ */ public void reset() { - Iterator iter = state.keySet().iterator(); - - while (iter.hasNext()) + if (buffer != null) { - setStateInt(iter.next(), UNINITIALIZED); + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null) + { + buffer[i].state = UNINITIALIZED; + buffer[i].value = null; + } + } } - - buffer.clear(); } /** @@ -923,19 +1039,19 @@ * * @param widgetID * The ID of the widget to which the data is related. + * * @return true if the widget has been modified during the * last input operation. */ public boolean isEntered(int widgetID) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) - { + int idx = idToIndex(widgetID); + + if (idx == -1) return false; - } // verify the state has ENTERED flag on - return (getStateInt(widgetID) & ENTERED) == ENTERED; + return (buffer[idx].state & ENTERED) == ENTERED; } /** @@ -948,14 +1064,13 @@ */ public boolean isChanged(int widgetID) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) - { + int idx = idToIndex(widgetID); + + if (idx == -1) return false; - } // verify the state is not UNCHANGED or UNINITIALIZED - return getStateInt(widgetID) != UNCHANGED && !isUninitialized(widgetID); + return buffer[idx].state != UNCHANGED && !((buffer[idx].state & UNINITIALIZED) == UNINITIALIZED); } /** @@ -968,16 +1083,16 @@ */ public boolean isUninitialized(int widgetID) { - // verify the widget ID is valid for this buffer - if (!isIdValid(widgetID)) + int idx = idToIndex(widgetID); + + if (idx == -1) { - // false means it is initialized which is worse, but this doesn't make much sense - // either - return true; + // false means it is initialized which is worse, but this doesn't make much sense either + return true; } // verify the state has UNINITIALIZED flag on - return (getStateInt(widgetID) & UNINITIALIZED) == UNINITIALIZED; + return (buffer[idx].state & UNINITIALIZED) == UNINITIALIZED; } /** @@ -1056,93 +1171,39 @@ } /** - * Debugging Aid. - * Visualizes the contents of this instance. + * Debugging aid to visualize the contents of this instance. * * @return The array of strings that represent the contents of the buffer. */ public String[] view() { - int lsize = size + 1 + buffer.size() + 1 + - (headers == null ? 0 : headers.size() + 1); - String[] s = new String[lsize]; - int i = 0; - - // state flags - s[0] = String.format("state[%d]:", new Object[] {size}); - - Iterator it = state.keySet().iterator(); - Integer id = null; - while (it.hasNext()) - { - StringBuffer sb = new StringBuffer(); - boolean f = false; - id = it.next(); - Byte bw = state.get(id); - byte b = (bw != null) ? bw.byteValue() : UNINITIALIZED; - - if (b >= ENTERED) - { - f = true; - sb.append("ENTERED "); - b -= ENTERED; - } - - if (b >= CHANGED) - { - f = true; - sb.append("CHANGED "); - b -= CHANGED; - } + ArrayList output = new ArrayList<>(); + + if (buffer != null) + { + output.add(String.format("buffer[%d]:", buffer.length)); + + for (int j = 0; j < buffer.length; j++) + { + output.add(" " + ((buffer[j] == null) ? "null" : buffer[j].toString())); + } + } + else + { + output.add("buffer == null"); + } + + if (headers != null) + { + output.add(String.format("headers[%d]:", headers.length)); - if (b >= UNINITIALIZED) - { - f = true; - sb.append("UNINITIALIZED "); - b -= UNINITIALIZED; - } - - if (!f) - { - sb.append("UNCHANGED "); - } - - s[i + 1] = String.format(" %d: %s", new Object[] {id.intValue(), sb.toString()}); - i++; - } - - // widgets - s[i++] = String.format("widgets[%d]:", new Object[] {buffer.size()}); - it = buffer.keySet().iterator(); - id = null; - Object v = null; - while (it.hasNext()) - { - id = it.next(); - v = buffer.get(id); - if (v instanceof BaseDataType) - { - v = ((BaseDataType)v).toStringMessage(); - } - s[i] = String.format(" %d: %s", new Object[] {id, v}); - i++; - } - - // headers - if (headers == null) - return s; - - s[i++] = String.format("headers[%d]:", new Object[] {headers.size()}); - it = headers.keySet().iterator(); - while (it.hasNext()) - { - id = it.next(); - v = headers.get(id); - s[i] = String.format(" %d: %s", new Object[] {id, v}); - i++; - } - - return s; + for (int j = 0; j < headers.length; j++) + { + output.add(String.format(" id = %d; value = %s", headers[j].id, headers[j].value)); + } + } + + return output.toArray(new String[0]); } /** @@ -1167,8 +1228,7 @@ } /** - * Replacement for the default object reading method. The latest - * state is read from the input source. + * Replacement for the default object reading method. The latest state is read from the input source. * * @param in * The input source from which fields will be restored. @@ -1184,6 +1244,8 @@ ClassNotFoundException { baseId = in.readInt(); + offset = in.readInt(); + size = in.readInt(); keyCode = in.readInt(); keyState = in.readByte(); down = in.readInt(); @@ -1191,31 +1253,41 @@ row = in.readInt(); column = in.readInt(); widgetId = in.readInt(); - size = in.readInt(); title = readString(in); frameValue = readString(in); - // convert the widget and header arrays back to maps - state = readMap(in, HashMap::new, NativeTypeSerializer::readInteger, NativeTypeSerializer::readByte); - buffer = readMap(in, HashMap::new, NativeTypeSerializer::readInteger, ObjectInput::readObject); - headers = readMap(in, HashMap::new, NativeTypeSerializer::readInteger, ObjectInput::readObject); + // convert the widget arrays back to maps + buffer = readSparseExtArray(in, StatefulWidgetRecord[]::new, StatefulWidgetRecord::new); + headers = readExtArray(in, WidgetRecord[]::new, WidgetRecord::new); values = readMap(in, HashMap::new, NativeTypeSerializer::readInteger, ObjectInput::readObject); + + if (buffer != null) + { + for (int i = 0; i < buffer.length; i++) + { + if (buffer[i] != null) + { + // detect non-contiguous mode and remember any non-contiguous id mappings + if (buffer[i].id - offset != i) + { + indices.put(buffer[i].id, i); + } + } + } + } - /** - * downConfigs contains instances of different subclasses of WidgetConfig - * hence the information about the class are added to the stream - */ + // downConfigs contains instances of different subclasses of WidgetConfig + // hence the information about the class are added to the stream downConfigs = readArray(in, WidgetConfig[]::new, s -> (WidgetConfig)s.readObject()); currentStatement = readEnum(in, UIStatement.values()); } /** - * Replacement for the default object writing method. The latest - * state is written to the output destination. + * Replacement for the default object writing method. Writes the current state to the output destination. * * @param out - * The output destination to which fields will be saved. + * The output destination to which fields will be written. * * @throws IOException * In case of I/O errors. @@ -1225,6 +1297,8 @@ throws IOException { out.writeInt(baseId); + out.writeInt(offset); + out.writeInt(size); out.writeInt(keyCode); out.writeByte(keyState); out.writeInt(down); @@ -1232,104 +1306,567 @@ out.writeInt(row); out.writeInt(column); out.writeInt(widgetId); - out.writeInt(size); writeString(out, title); writeString(out, frameValue); - writeMap(out, state, NativeTypeSerializer::writeInteger, NativeTypeSerializer::writeByte); - writeMap(out, buffer, NativeTypeSerializer::writeInteger, ObjectOutput::writeObject); - writeMap(out, headers, NativeTypeSerializer::writeInteger, ObjectOutput::writeObject); + writeSparseArray(out, buffer); + writeArray(out, headers); writeMap(out, values, NativeTypeSerializer::writeInteger, ObjectOutput::writeObject); - /** - * downConfigs contains instances of different subclasses of WidgetConfig - * hence the information about the class should be added to the stream - */ + + // downConfigs contains instances of different subclasses of WidgetConfig + // hence the information about the class should be added to the stream writeArray(out, ObjectOutput::writeObject, downConfigs); writeEnum(out, currentStatement); } - - /** - * Checks the ID to be used inside this screen buffer. - * - * @param id - * The ID of the widget to verify. - * @return true if the widget ID can be used inside this screen buffer - * false otherwise. - */ - public boolean isIdValid(int id) - { - return state.containsKey(id) || state.size() < size; - } - - /** - * Makes bitwise AND operation with the given widget ID state according to mask provided. The - * state byte after operation is storing back to the state map. - * - * @param id - * The ID of the widget to reset status. - * @param mask - * The mask to perform bitwise and operation with the current one. - */ - private void bitwiseAndState(int id, byte mask) - { - // if status exists - state.computeIfPresent(id, (k, v) -> - { - // get current status - byte stateNew = v.byteValue(); - // change it - stateNew &= mask; - // store it back - return stateNew; - }); - } - - /** - * Gets the current state for the given widget ID. Assuming the widget ID is valid for this - * screen buffer. Internal version of the method. - * - * @param id - * The ID of the widget to which the state byte to be returned. - * - * @return State of this widget or UNINITIALIZED if such ID does not exist in - * current state map. - */ - private byte getStateInt(int id) - { - // get the current status - Byte stateByte = state.get(id); - - return (stateByte != null) ? stateByte.byteValue() : UNINITIALIZED; - } - - /** - * Stores the new state for the widget with the given widget ID. Internal version. If the - * new state is UNINITIALIZED, the previous value of the widget and state, if any, is deleted. - * - * @param id - * The ID of the widget to which the state byte is to be stored. - * @param stateNew - * new state - */ - private void setStateInt(int id, byte stateNew) - { - // state value should be kept in buffer even if widget become uninitialized or removed - state.put(id, stateNew); - } - - /** - * Copies the state info map from the current state to target screen buffer. No checking, just - * copy existed map entries - * - * @param target - * a ScreenBuffer to copy state map info to. - */ - private void copyState(ScreenBuffer target) - { - Iterator iter = state.keySet().iterator(); - while (iter.hasNext()) - { - Integer nextId = iter.next(); - target.state.put(nextId, this.state.get(nextId)); + + /** + * Ensure that there is enough capacity in the widget array and return a valid, empty array index. + * + * @param widgetID + * The ID of the widget to which the data is related. + * + * @return A valid, empty array index in which the next widget should be stored. + */ + private int calcNextIndex(int widgetID) + { + int possible = -1; + + // allocate the array if it was not otherwise allocated or if it is zero-sized; some callers + // (dynamic frames) instantiate using permissive mode and numWidgets == 0 which allocates a + // zero-sized array + if (buffer == null || buffer.length == 0) + { + buffer = new StatefulWidgetRecord[MIN_ELEMENTS]; + offset = widgetID; + return 0; + } + else + { + // buffer exists but it is an empty array, initialize contiguous mode + if (size == 0) + { + // if the offset was not already initialized, do it here (assumes the first widget is the min + // value) + if (offset == -1) + { + offset = widgetID; + return 0; + } + } + + // non-empty array try an offset index + possible = offsetIndex(widgetID); + + if (possible == -1) + { + // the offset can't work with this ID + if (reallocate(widgetID)) + { + // we have a bigger buffer that should now work in contiguous mode + possible = offsetIndex(widgetID); + } + } + + if (possible != -1 && buffer[possible] == null) + { + return possible; + } + } + + // contiguous mode can't be used, just find an open element + // DEBUG System.err.printf("ScreenBuffer CONTIGUOUS MODE UNAVAILABLE; wid = %d; possible = %d; size = %d; offset = %d; buffer.length = %d\n", widgetID, possible, size, offset, buffer.length); + + // out of space, enlarge + if (size == buffer.length) + { + buffer = Arrays.copyOf(buffer, buffer.length + MIN_ELEMENTS); + } + + // search from the top down (we often have padding on that end) + for (int i = buffer.length - 1; i >= 0; i--) + { + if (buffer[i] == null) + { + return i; + } + } + + // should not happen + // DEBUG System.err.printf("ScreenBuffer calcNextIndex() FAILURE; wid = %d; size = %d; offset = %d; buffer.length = %d\n", widgetID, size, offset, buffer.length); + return -1; + } + + /** + * Reallocate the buffer to accommodate the given widget in contiguous mode. This should only be called + * if the buffer is already allocated AND the widget ID cannot be accommodated already. + * + * @param wid + * Widget ID. + */ + private boolean reallocate(int wid) + { + int sz = 0; + + // the offset (which is the minimum widget id that can be indexed currently) can't work with this + // smaller ID + if (wid < offset) + { + int missing = offset - wid; + int needed = buffer.length + missing; + + // DEBUG + // System.err.printf("ScreenBuffer reallocate() FRONT; wid = %d; missing = %d; needed = %d;" + + // " size = %d; offset = %d; buffer.length = %d\n", + // wid, + // missing, + // needed, + // size, + // offset, + // buffer.length); + + if (needed > MAX_SPARSE_ARRAY_SIZE) + { + // no space to enlarge + if (size != buffer.length) + { + int avail = 0; + + // calculate how much empty space is at the end of the array + for (int i = buffer.length - 1; i >= 0; i--) + { + if (buffer[i] != null) + { + break; + } + else + { + avail++; + } + } + + // DEBUG + // System.err.printf("ScreenBuffer reallocate() SHIFT; wid = %d; missing = %d; needed = %d;" + + // " avail = %d; size = %d; offset = %d; buffer.length = %d\n", + // wid, + // missing, + // needed, + // avail, + // size, + // offset, + // buffer.length); + + // shift the existing entries if there is space at the end + if (avail >= missing) + { + for (int k = buffer.length - (avail + 1); k >= 0; k--) + { + buffer[k + avail] = buffer[k]; + } + Arrays.fill(buffer, 0, avail, null); + offset = wid; + } + else + { + // the array is sparse, but the first and last widgets are too far apart to shift + return false; + } + } + else + { + // the array is full and the buffer length is already at the size limit + return false; + } + } + else + { + // try to enlarge from the beginning + int min = wid; + int max = offset + buffer.length; + + sz = calcWidgetArraySize(min, max); + + // DEBUG + // System.err.printf("ScreenBuffer reallocate() ENLARGE FRONT; wid = %d; min = %d; max = %d;" + + // " sz = %d; size = %d; offset = %d; buffer.length = %d\n", + // wid, + // min, + // max, + // sz, + // size, + // offset, + // buffer.length); + + if (sz != buffer.length) + { + StatefulWidgetRecord[] replacement = new StatefulWidgetRecord[sz]; + System.arraycopy(buffer, 0, replacement, offset - min, buffer.length); + + buffer = replacement; + offset = min; + } + else + { + // reallocation failed + return false; + } + } + } + else + { + // enlarge from the end + sz = calcWidgetArraySize(offset, wid + MIN_ELEMENTS); + + // DEBUG + // System.err.printf("ScreenBuffer reallocate() ENLARGE BACK; wid = %d; min = %d, max = %d;" + + // " sz = %d; size = %d; offset = %d; buffer.length = %d\n", + // wid, + // offset, + // wid + MIN_ELEMENTS, + // sz, + // size, + // offset, + // buffer.length); + + if (sz != buffer.length) + { + buffer = Arrays.copyOf(buffer, sz); + } + else + { + // reallocation failed + return false; + } + } + + return true; + } + + /** + * Add an uninitialized widget to the buffer at the given index. No error checking is done. The buffer + * index must be valid and unused. + * + * @param id + * The widget ID to add. + * @param idx + * The element index at which to add. + */ + private void addWidgetInt(int id, int idx) + { + // if not in contiguous mode, we need to add the id to the map + if (idx + offset != id) + { + // DEBUG System.err.printf("ScreenBuffer MAP PUT; wid = %d; idx = %d; size = %d; offset = %d; buffer.length = %d; map size = %d\n", id, idx, size, offset, buffer.length, indices.size()); + indices.put(id, idx); + } + + buffer[idx] = new StatefulWidgetRecord(id, null, UNINITIALIZED); + size++; + } + + /** + * Remove the widget at the given index position. + * + * @param idx + * The array index to remove. + */ + private void removeWidgetInt(int idx) + { + if (buffer != null && idx >= 0 && idx < buffer.length && buffer[idx] != null) + { + // remove the value itself + values.remove(buffer[idx].id); + + if (idx + offset != buffer[idx].id) + { + indices.remove(buffer[idx].id); + } + + buffer[idx] = null; + size--; + } + } + + /** + * The array index of the given widget. + * + * @param id + * The widget to lookup. + * + * @return The array index or -1 if the widget is not present in this screen buffer. + */ + private int idToIndex(int id) + { + // very quick out, there are no ids + if (buffer == null || size == 0) + return -1; + + // next try the offset mode, contiguous ids added starting at index 0 can be found this way + int possible = offsetIndex(id); + + if (possible != -1) + { + if (buffer[possible] != null && buffer[possible].id == id) + { + return possible; + } + } + + // fallback approach for non-contiguous ids + Integer idx = indices.get(id); + + return (idx == null || buffer[idx] == null) ? -1 : idx; + } + + /** + * Calculate the index for the given widget in contiguous node. + * + * @param id + * The widget ID. + * + * @return The valid contiguous mode index or -1 if the widget cannot be used in this mode. + */ + private int offsetIndex(int id) + { + if (offset != -1 && buffer != null) + { + int possible = id - offset; + + if (possible >= 0 && possible < buffer.length) + { + return possible; + } + } + + return -1; + } + + /** + * Calculate the buffer array size based on the expected widget ID range. If possible, the array will + * be sized to allow all widgets to be stored/retried via offset mode. + * + * @param min + * The smallest widget ID to be used for this screen buffer. + * @param max + * The largest widget ID to be used for this screen buffer. + */ + private int calcWidgetArraySize(int min, int max) + { + int size = (max - min) + 1; + int aligned = ((size % MIN_ELEMENTS) == 0) ? size : ((size / MIN_ELEMENTS) + 1) * MIN_ELEMENTS; + + // DEBUG System.err.printf("ScreenBuffer calcWidgetArraySize() min = %d; max = %d; size = %d; aligned = %d\n", min, max, size, aligned); + + return (aligned <= MAX_SPARSE_ARRAY_SIZE) ? aligned : MAX_SPARSE_ARRAY_SIZE; + } + + /** + * Stores the details of a given widget. + */ + private static class WidgetRecord + implements Externalizable + { + /** Widget ID for this widget. */ + int id = -1; + + /** Value for the widget. */ + Object value = null; + + /** + * Default constructor. + */ + WidgetRecord() + { + } + + /** + * Constructor. + * + * @param id + * The widget ID. + * @param value + * The widget value. + */ + WidgetRecord(int id, Object value) + { + this.id = id; + this.value = value; + } + + /** + * Copy constructor. + * + * @param other + * The instance to copy. + */ + WidgetRecord(WidgetRecord other) + { + this.id = other.id; + this.value = other.value; + } + + /** + * Render the instance as text. + * + * @return The rendered text. + */ + @Override + public String toString() + { + return String.format("id: %d; value: %s", id, value); + } + + /** + * Replacement for the default object reading method. The latest state is read from the input source. + * + * @param in + * The input source from which fields will be restored. + * + * @throws IOException + * In case of I/O errors. + * @throws ClassNotFoundException + * If payload can't be instantiated. + */ + @Override + public void readExternal(ObjectInput in) + throws IOException, + ClassNotFoundException + { + id = in.readInt(); + value = in.readObject(); + } + + /** + * Replacement for the default object writing method. Writes the current state to the output destination. + * + * @param out + * The output destination to which fields will be written. + * + * @throws IOException + * In case of I/O errors. + */ + @Override + public void writeExternal(ObjectOutput out) + throws IOException + { + out.writeInt(id); + out.writeObject(value); + } + } + + /** + * Adds a state bitflag to a widget record. + */ + private static class StatefulWidgetRecord + extends WidgetRecord + { + /** Current state for the widget. */ + byte state = UNINITIALIZED; + + /** + * Default constructor. + */ + StatefulWidgetRecord() + { + } + + /** + * Constructor. + * + * @param id + * The widget ID. + * @param value + * The widget value. + */ + StatefulWidgetRecord(int id, BaseDataType value, byte state) + { + super(id, value); + this.state = state; + } + + /** + * Copy constructor. + * + * @param other + * The instance to copy. + */ + StatefulWidgetRecord(StatefulWidgetRecord other) + { + super(other); + this.state = other.state; + } + + /** + * Render the instance as text. + * + * @return The rendered text. + */ + @Override + public String toString() + { + StringBuffer sb = new StringBuffer(); + + boolean f = false; + + if ((state & ENTERED) == ENTERED) + { + f = true; + sb.append("ENTERED "); + } + + if ((state & CHANGED) == CHANGED) + { + f = true; + sb.append("CHANGED "); + } + + if ((state & UNINITIALIZED) == UNINITIALIZED) + { + f = true; + sb.append("UNINITIALIZED"); + } + + if (!f) + { + sb.append("UNCHANGED"); + } + + return String.format("%s; state [%s]", super.toString(), sb.toString()); + } + + /** + * Replacement for the default object reading method. The latest state is read from the input source. + * + * @param in + * The input source from which fields will be restored. + * + * @throws IOException + * In case of I/O errors. + * @throws ClassNotFoundException + * If payload can't be instantiated. + */ + @Override + public void readExternal(ObjectInput in) + throws IOException, + ClassNotFoundException + { + super.readExternal(in); + state = in.readByte(); + } + + /** + * Replacement for the default object writing method. Writes the current state to the output destination. + * + * @param out + * The output destination to which fields will be written. + * + * @throws IOException + * In case of I/O errors. + */ + @Override + public void writeExternal(ObjectOutput out) + throws IOException + { + super.writeExternal(out); + out.writeByte(state); } } } === modified file 'src/com/goldencode/p2j/ui/ScreenDefinition.java' (properties changed: -x to +x) --- src/com/goldencode/p2j/ui/ScreenDefinition.java 2020-09-17 11:46:00 +0000 +++ src/com/goldencode/p2j/ui/ScreenDefinition.java 2020-11-17 20:03:34 +0000 @@ -56,6 +56,7 @@ ** 020 CA 20151026 Added support for nested frames. ** 021 HC 20191111 Implemented widget delete on the client. ** 022 IAS 20200901 Rework (de)serialization. +** 023 CA 20201117 Removed the 'deletedWidgets' field, as that is no longer used. */ /* ** This program is free software: you can redistribute it and/or modify @@ -143,9 +144,6 @@ /** For each side-label, specify its parent. */ private Map sideLabelParents = new HashMap<>(); - /** List of deleted widgets */ - private List deletedWidgets; - /** * Default constructor (only used for serialization). */ @@ -313,32 +311,6 @@ } /** - * Adds widget id to the list of deleted widgets. - * - * @param widgetId - * Id of the deleted widget. - */ - public void addDeletedWidget(int widgetId) - { - if (deletedWidgets == null) - { - deletedWidgets = new ArrayList<>(); - } - - deletedWidgets.add(widgetId); - } - - /** - * Returns list of deleted widgets. - * - * @return see above. - */ - public List getDeletedWidgets() - { - return deletedWidgets == null ? Collections.emptyList() : Collections.unmodifiableList(deletedWidgets); - } - - /** * Replacement for the default object reading method. The latest * state is read from the input source. * @@ -357,11 +329,8 @@ { id = in.readInt(); suppressRedraw = in.readBoolean(); - WidgetEntry.arrayToMap(readExtArray(in, WidgetEntry[]::new, WidgetEntry::new), - widgets); - WidgetEntry.arrayToMap(readExtArray(in, WidgetEntry[]::new, WidgetEntry::new), - sideLabels); - deletedWidgets = readList(in, s -> readInteger(s)); + WidgetEntry.arrayToMap(readExtArray(in, WidgetEntry[]::new, WidgetEntry::new), widgets); + WidgetEntry.arrayToMap(readExtArray(in, WidgetEntry[]::new, WidgetEntry::new), sideLabels); } /** @@ -382,7 +351,6 @@ out.writeBoolean(suppressRedraw); writeArray(out, WidgetEntry.mapToArray(widgets)); writeArray(out, WidgetEntry.mapToArray(sideLabels)); - writeList(out, (o, v) -> writeInteger(o, v), deletedWidgets); } /** === modified file 'src/com/goldencode/p2j/ui/SelectionListConfig.java' --- src/com/goldencode/p2j/ui/SelectionListConfig.java 2020-09-29 02:32:29 +0000 +++ src/com/goldencode/p2j/ui/SelectionListConfig.java 2020-12-08 10:16:22 +0000 @@ -38,6 +38,7 @@ ** 020 HC 20150323 Removed business logic from config classes. ** 021 IAS 20150604 Changed default value for the innerLines field. ** 022 EVL 20200925 Pack all boolean attributes into single 32-bit integer for socket read/write. +** 023 CA 20201207 Changed default value for the innerChars field. */ /* ** This program is free software: you can redistribute it and/or modify @@ -103,7 +104,7 @@ extends ControlSetConfig { /** The width in chars of the item list. */ - public int innerChars = 10; + public int innerChars = -1; /** The number of text lines in the widget. */ public int innerLines = -1; @@ -244,4 +245,4 @@ out.writeInt(booleanAttrs); } -} \ No newline at end of file +} === modified file 'src/com/goldencode/p2j/ui/SelectionListWidget.java' --- src/com/goldencode/p2j/ui/SelectionListWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/SelectionListWidget.java 2021-01-29 09:30:09 +0000 @@ -2,7 +2,7 @@ ** Module : SelectionListWidget.java ** Abstract : server side selection list widget implementation ** -** Copyright (c) 2005-2018, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description---------------------------- ** 001 NVS 20051026 @23495 Created initial version. @@ -44,6 +44,9 @@ ** 024 SBI 20180503 Moved validateLayout() to the client side and fixed setInnerLines( ** double). ** 025 EVL 20180809 Adding SELECTION-LIST specific for setting list item pairs. +** 026 EVL 20201022 Optimized attributes flush implementation. +** CA 20210128 Fixed a regression related to RADIO-BOX and SELECTION-LIST - these must not check +** the FORMAT, as they do not manage it. */ /* ** This program is free software: you can redistribute it and/or modify @@ -145,7 +148,7 @@ */ public integer getInnerChars() { - return new integer(getAttr("innerChars", () -> config.innerChars)); + return new integer(getAttr("innerChars", () -> config.innerChars, true)); } /** @@ -197,7 +200,7 @@ type()), false); } - return new integer(Math.max(0, getAttr("innerLines", () -> config.innerLines))); + return new integer(Math.max(0, getAttr("innerLines", () -> config.innerLines, true))); } /** @@ -587,7 +590,7 @@ public void setFrame(FrameWidget frame) { super.setFrame(frame); - if (getAttr("innerLines", () -> config.innerLines) < 0) + if (getAttr("innerLines", () -> config.innerLines, true) < 0) { config.innerLines = getAttr("items", () -> config.items) == null ? 3 : config.items.length; config.heightChars = config.innerLines + 2; @@ -698,7 +701,7 @@ @Override public void setListItemPairs(character list) { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { super.setListItemPairs(list); } @@ -709,6 +712,52 @@ } /** + * Sets the format specification. + * + * @param format + * The format specification. + */ + @LegacyAttribute(name = "FORMAT", setter = true, ignore = true) + @Override + public void setFormat(String format) + { + if (!config.dynamic && (frame == null || frame.isInsideSetup())) + { + return; + } + + notSettable("FORM", true); + } + + /** + * Gets the format specification. + * + * @return The format specification. + */ + @Override + @LegacyAttribute(name = "FORMAT", ignore = true) + public character getFormat() + { + if (!config.dynamic && (frame == null || frame.isInsideSetup())) + { + return new character(); + } + + notQueryable("FORM", true); + return new character(); + } + + /** + * Checks if the given widget has valid format string or even has the format attribute. + * + * @return always FALSE. + */ + public boolean hasFormat() + { + return false; + } + + /** * Internal worker for getting the SCREEN-VALUE on a per-widget basis. * * @param initialized === modified file 'src/com/goldencode/p2j/ui/SliderWidget.java' --- src/com/goldencode/p2j/ui/SliderWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/SliderWidget.java 2021-01-12 18:46:50 +0000 @@ -2,9 +2,9 @@ ** Module : SliderWidget.java ** Abstract : Server side slider widget ** -** Copyright (c) 2015-2018, Golden Code Development Corporation. +** Copyright (c) 2015-2021, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description--------------------------------- +** -#- -I- --Date-- ---------------------------------------Description--------------------------------------- ** 001 EVL 20150911 Created initial version. ** 002 EVL 20160127 Adding dynamic widget constructor. Default size calculations. ** The NO-CURRENT-VALUE should be inverted while setting to reflect 4GL logic. @@ -15,6 +15,8 @@ ** 006 CA 20180130 Replaced pushScreenDefinition() with pushWidgetAttr() for cases when business ** logic is assigning a widget attribute. ** 007 OM 20180713 Fixed TIC-MARKS attribute. +** 008 CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal usage, +** within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). */ /* @@ -528,7 +530,7 @@ * the value as uninitialized. */ @Override - public void setScreenValue(character value) + protected void setScreenValueInt(character value) { if (value != null && !value.isUnknown()) { === modified file 'src/com/goldencode/p2j/ui/SubMenuWidget.java' --- src/com/goldencode/p2j/ui/SubMenuWidget.java 2020-09-28 10:09:11 +0000 +++ src/com/goldencode/p2j/ui/SubMenuWidget.java 2020-10-22 14:25:52 +0000 @@ -2,7 +2,7 @@ ** Module : SubMenuWidget.java ** Abstract : server side sub-menu widget implementation ** -** Copyright (c) 2015-2019, Golden Code Development Corporation. +** Copyright (c) 2015-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ----------------------------Description----------------------------- ** 001 VIG 20141124 Created initial version. @@ -22,6 +22,9 @@ ** 011 CA 20190308 Partial menu sub-trees can be pushed, we need to push the entire menu tree to ** the client-side. ** CA 20190322 Avoid unnecessary client-side trips if the attribute isn't changing on set. +** 012 HC 20201010 Implemented selective config flushing. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. */ /* ** This program is free software: you can redistribute it and/or modify @@ -111,8 +114,9 @@ */ public integer getMnemonic() { - return getAttr("mnemonic", (Supplier)() -> config.mnemonic) == null ? new integer() : - new integer(getAttr("mnemonic", (Supplier)() -> config.mnemonic)); + return getAttr("mnemonic", + (Supplier)() -> (config.mnemonic == null ? new integer() + : new integer(config.mnemonic)), true); } /** @@ -123,9 +127,8 @@ */ public character getPreprocessedLabel() { - return getAttr("preprocessedLabel", () -> config.preprocessedLabel) == null - ? new character() - : new character(getAttr("preprocessedLabel", () -> config.preprocessedLabel)); + return getAttr("preprocessedLabel", () -> (config.preprocessedLabel == null ? new character() : + new character(config.preprocessedLabel)), true); } /** === modified file 'src/com/goldencode/p2j/ui/TabSetWidget.java' --- src/com/goldencode/p2j/ui/TabSetWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/TabSetWidget.java 2020-10-22 14:25:52 +0000 @@ -16,6 +16,8 @@ ** Added TabSet show() method. ** Added ocxId to discriminate among OCX's ** 004 AIL 20200406 Fixed the reset of tab index when setting the tabs. +** 005 HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. */ /* ** This program is free software: you can redistribute it and/or modify @@ -86,7 +88,7 @@ { /** Separator for TABSET item names. */ private final static String TABSET_NAME_SEPARATOR = ","; - + /** * Default constructor. */ === added file 'src/com/goldencode/p2j/ui/TextBasedWidget.java' --- src/com/goldencode/p2j/ui/TextBasedWidget.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/ui/TextBasedWidget.java 2021-01-25 13:46:58 +0000 @@ -0,0 +1,406 @@ +/* +** Module : TextBasedWidget.java +** Abstract : Base class for server-side literal or text widgets. +** +** Copyright (c) 2020-2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- --------------------------------------Description----------------------------------------- +** 001 HC 20201024 Created initial version. +** CA 20201202 LITERAL and TEXT must handle LABEL attribute individually. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal usage, +** within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). +** CA 20210121 LITERAL and TEXT must handle LABEL attribute individually (for the setter). +** CA 20210125 updateSideLabelColumn() is limited only for ChUI. +*/ +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ +package com.goldencode.p2j.ui; + +import com.goldencode.p2j.util.*; + +import java.util.*; + +/** + * Represents a LITERAL or TEXT widget on server-side. + */ +public abstract class TextBasedWidget +extends ControlTextWidget +{ + /** Side label discriminator, when true this instance represents a side label. */ + protected boolean sideLabel = false; + + /** The associated side widget for this label (when {@link #sideLabel} is true). */ + private GenericWidget sideWidget = null; + + /** + * Default constructor. + */ + public TextBasedWidget() + { + super(false, new TextConfig()); + setStatic(true); + } + + /** + * A custom getter for the LABEL attribute. In case of LITERAL widgets, this attribute is not queryable. + * For TEXT widgets, the LABEL attribute is allowed. + * + * @return The label text. + */ + protected abstract character getLabelWorker(); + + /** + * A custom setter for the LABEL attribute. In case of LITERAL widgets, this attribute is not settable. + * For TEXT widgets, the LABEL attribute is allowed. + * + * @param value + * The label. + */ + protected abstract void setLabelWorker(String value); + + /** + * Default constructor. + * + * @param dynamic + * Flag indicating if this is a static or dynamic resource. + */ + public TextBasedWidget(boolean dynamic) + { + super(dynamic, new TextConfig()); + } + + /** + * Obtains the next sibling widget in the current field group. + * + * @return The next sibling or an unknown handle if this is the only widget or if this is the + * last widget. + */ + @Override + public handle getNextSibling() + { + if (sideLabel && group instanceof FieldGroup) + { + // find the related widget + for (GenericWidget w : ((FieldGroup) group).getWidgets()) + { + if (!(w.config instanceof ControlConfig)) + { + continue; + } + + if (config.id.asInt() == ((ControlConfig) w.config).sideLabelId) + { + return new handle(w); + } + } + } + + return super.getNextSibling(); + } + + /** + * Obtains the previous sibling widget in the current field group. + * + * @return The previous sibling or an unknown handle if this only widget or if this is the + * first widget. + */ + @Override + public handle getPrevSibling() + { + if (sideLabel && group instanceof FieldGroup) + { + // find the related widget + for (GenericWidget w : ((FieldGroup) group).getWidgets()) + { + if (!(w.config instanceof ControlConfig)) + { + continue; + } + + if (config.id.asInt() == ((ControlConfig) w.config).sideLabelId) + { + return new handle(((FieldGroup) group).getPrevChild(w)); + } + } + } + + return super.getPrevSibling(); + } + + /** + * Get the label text for this widget. + *

+ * The implementation is redirected to {@link #getLabelWorker()}. + * + * @return The empty string. + */ + @Override + public character getLabel() + { + return getLabelWorker(); + } + + /** + * Set the label text for this widget. + *

+ * The implementation is redirected to {@link #setLabelWorker()}. + * + * @param label + * The new label text. + */ + @Override + public void setLabel(String label) + { + setLabelWorker(label); + } + + /** + * Obtain the current value in the screen buffer of the backing data for + * this widget and return it as a character type. If + * there is no value in the screen buffer (if it is null + * which is the representation for an uninitialized value), then return + * the empty string. + * + * @return The value as a character type or the empty + * string if the screen buffer value is uninitialized. + */ + @Override + public character getScreenValue() + { + if (!sideLabel) + { + return super.getScreenValue(); + } + + return sideWidget.getLabel(); + } + + /** + * Set the current value in the screen buffer of the backing data for + * this widget. If the given value is null then this + * widget will be set to the uninitialized value. + * + * @param value + * The new value for the widget, use null to set + * the value as uninitialized. + */ + @Override + protected void setScreenValueInt(character value) + { + if (!sideLabel) + { + super.setScreenValueInt(value); + } + else + { + sideWidget.setLabel(value); + } + } + + /** + * Make this widget into a side-label. + * + * @param sideWidget + * The widget the side-label belongs to. + */ + public void makeSideLabel(GenericWidget sideWidget) + { + this.sideLabel = true; + this.sideWidget = sideWidget; + } + + /** + * Clears the side-label flags. + */ + public void clearSideLabel() + { + this.sideLabel = false; + this.sideWidget = null; + } + + /** + * Sets the row or column position for this widget. + *

+ * Sub-classes can override this method; at this point, the parameters are valid. + * + * @param isUnknown + * If true the attribute value being set is an unknown value. + * @param value + * The 1-based row or column number. + * @param row + * If true value represents a row number, otherwise + * a column number. + */ + @Override + protected void setColumnOrRowWorker(boolean isUnknown, double value, boolean row) + { + int align = config.align; + + super.setColumnOrRowWorker(isUnknown, value, row); + + // preserve alignment when changing side label position + if (sideLabel) + { + config.align = align; + } + + if (row) + { + // nothing to do for rows + return; + } + + updateSideLabelColumn(value); + } + + /** + * Sets the x or y pixel position for this widget. + *

+ * Sub-classes can override this method; at this point, the parameters are + * valid. + * + * @param isUnknown + * If + * true the attribute value being set is an unknown value. + * @param value + * The x or y pixel value. + * @param x + * If true value represents an x + * position, otherwise y. + */ + @Override + protected void setXOrYWorker(boolean isUnknown, int value, boolean x) + { + int align = config.align; + + super.setXOrYWorker(isUnknown, value, x); + + // preserve alignment when changing side label position + if (sideLabel) + { + config.align = align; + } + + if (!x) + { + // nothing to do for y pixel position change + return; + } + + // use the converted column value set in the super implementation + updateSideLabelColumn(config.column); + } + + /** + * Collect the size-related attributes which can be assigned by this widget. + * + * @param names + * The collection where to add the field names. + * @param vals + * The collection where to add the field values. + */ + @Override + protected void getSizeAttrs(List names, List vals) + { + if (sideLabel) + { + names.addAll(Arrays.asList("fixedWidth", "fixedHeight", + "heightChars", "widthChars", + "heightPixels", "widthPixels", + "clientHeightChars", "clientWidthChars", + "clientHeightPixels", "clientWidthPixels")); + vals.addAll(Arrays.asList(config.fixedWidth, config.fixedHeight, + config.heightChars, config.widthChars, + config.heightPixels, config.widthPixels, + config.clientHeightChars, config.clientWidthChars, + config.clientHeightPixels, config.clientWidthPixels)); + } + else + { + super.getSizeAttrs(names, vals); + } + } + + /** + * The method updates side label metrics after a change of column + * position. If this widget doesn't represent a side label, this + * method is a no-op. + * + * @param col + * New widget column position. + */ + private void updateSideLabelColumn(double col) + { + // the following code is ChUI specific. In GUI, the width must follow the pixels... + if (!sideLabel || !LogicalTerminal.isChui()) + { + return; + } + + String name = getAttr("name", () -> config.name); + + // if the literal is not yet realized, compute its width + if (!getAttr("realized", () -> config.realized) && name != null && getAttr("align", () -> config.align) == ControlEntity.ALIGN_RIGHT) + { + if (name.length() > col - 1) + { + config.column = 0; + config.widthChars = col - 1; + } + else + { + config.widthChars = name.length() + 1; + } + } + } +} === modified file 'src/com/goldencode/p2j/ui/TextConfig.java' --- src/com/goldencode/p2j/ui/TextConfig.java 2017-04-01 23:33:34 +0000 +++ src/com/goldencode/p2j/ui/TextConfig.java 2020-12-02 19:15:14 +0000 @@ -2,7 +2,7 @@ ** Module : TextConfig.java ** Abstract : stores TEXT specific state ** -** Copyright (c) 2014-2017, Golden Code Development Corporation. +** Copyright (c) 2014-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 CA 20140924 Created initial version. @@ -11,6 +11,8 @@ ** 003 CA 20150722 In LABEL case, the server and client-side configs are different types *** (TextConfig vs LabelConfig) - so, applyConfig is protected to ignore instances ** which are not of this type. +** 004 CA 20201202 Added 'selfSideLabel', a quirk for 4GL's 'feature' where a TEXT:SIDE-LABEL-HANDLE can be +** the same as the owner widget. */ /* ** This program is free software: you can redistribute it and/or modify @@ -78,6 +80,9 @@ /** Flag indicating this widget is using an explicit VIEW-AS TEXT phrase. */ public boolean explicitViewAs; + /** Special flag when the SIDE-LABEL-HANDLE is the same widget as the owner widget. */ + public boolean selfSideLabel = false; + /** * Default constructor (only used in deserialization). */ @@ -126,6 +131,7 @@ TextConfig cfg = (TextConfig) config; explicitViewAs = cfg.explicitViewAs; + selfSideLabel = cfg.selfSideLabel; } /** @@ -148,6 +154,7 @@ { super.readExternal(in); explicitViewAs = in.readBoolean(); + selfSideLabel = in.readBoolean(); } /** @@ -167,5 +174,6 @@ { super.writeExternal(out); out.writeBoolean(explicitViewAs); + out.writeBoolean(selfSideLabel); } } \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/TextWidget.java' --- src/com/goldencode/p2j/ui/TextWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/TextWidget.java 2021-01-22 11:28:28 +0000 @@ -2,7 +2,7 @@ ** Module : TextWidget.java ** Abstract : server-side text widget representation. ** -** Copyright (c) 2013-2019, Golden Code Development Corporation. +** Copyright (c) 2013-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --------------------------------Description----------------------------------- ** 001 CA 20130927 Created initial version. @@ -19,6 +19,11 @@ ** 007 CA 20180130 Replaced pushScreenDefinition() with pushWidgetAttr() for cases when business ** logic is assigning a widget attribute. ** 008 CA 20190301 Added LABELS attribute (read-only). +** 009 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** CA 20201202 Added 'selfSideLabel', a quirk for 4GL's 'feature' where a TEXT:SIDE-LABEL-HANDLE can be +** the same as the owner widget. +** TEXT:LABEL is a queryable attribute. +** CA 20210121 TEXT:LABEL is a settable attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -87,7 +92,7 @@ */ @LegacyResource(resource = LegacyResource.TEXT) public class TextWidget -extends ControlTextWidget +extends TextBasedWidget implements BlankInterface, Labels { @@ -107,7 +112,7 @@ */ public TextWidget(boolean dynamic) { - super(dynamic, new TextConfig()); + super(dynamic); setStatic(false); } @@ -122,7 +127,50 @@ { config.explicitViewAs = explicit; } - + + /** + * Sets the side-label to the specified one. A special case is treated here: if the label is the actual + * widget instance for which the SIDE-LABEL-HANDLE attribute is set, the the special + * {@link TextConfig#selfSideLabel} will be set to true. This is because 4GL allows the + * SIDE-LABEL-HANDLE to be set as the same 'owner' widget. + * + * @param label + * The label to set. + */ + @Override + public void setSideLabelHandle(handle label) + { + // set the 'labels' attribute, too + config.labels = !label.isUnknown(); + + config.selfSideLabel = false; + + if (!label.isUnknown() && label.get() == this) + { + config.selfSideLabel = true; + return; + } + + super.setSideLabelHandle(label); + } + + /** + * Get the side-label for this widget. If the {@link TextConfig#selfSideLabel} flag is set, then this + * widget will be returned. Otherwise, the call is delegated to the super-class. + * + * @return The handle to the text widget that is the side label of this widget. + */ + @Override + public handle getSideLabelHandle() + { + if (config.selfSideLabel) + { + return new handle(this); + } + + return super.getSideLabelHandle(); + } + /** * Set value of the BLANK attribute. * @@ -193,4 +241,33 @@ { return new logical(getAttr("labels", () -> config.labels) && hasSideLabelHandle()); } + + /** + * A custom getter for the LABEL attribute. In case of TEXT widgets, this attribute returns the actual + * LABEL value. + * + * @return The label value. + */ + @Override + protected character getLabelWorker() + { + flushWidgetAttrs(); + return new character(config.getDynamicLabel()); + } + + /** + * A custom setter for the LABEL attribute. In case of TEXT widgets, this attribute is allowed to be + * changed. + * + * @param value + * The label. + */ + @Override + protected void setLabelWorker(String value) + { + if (validateLabelAssignment()) + { + setLabelInt(value); + } + } } === modified file 'src/com/goldencode/p2j/ui/ToggleBoxConfig.java' --- src/com/goldencode/p2j/ui/ToggleBoxConfig.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/ToggleBoxConfig.java 2021-01-22 11:28:28 +0000 @@ -2,7 +2,7 @@ ** Module : ToggleBoxConfig.java ** Abstract : ToggleBox widget configuration. ** -** Copyright (c) 2010-2020, Golden Code Development Corporation. +** Copyright (c) 2010-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 SVL 20101004 Created initial version. @@ -18,6 +18,7 @@ ** 021 HC 20150323 Removed business logic from config classes. ** 022 IAS 20151214 MOUSE-POINTER attribute support. ** 023 IAS 20200823 Rework (de)serialization. +** CA 20210122 Added FORMAT attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -88,6 +89,9 @@ /** MOUSE-POINTER attribute */ public String mousePointer = null; + + /** The Progress FORMAT attribute. */ + public String format = null; /** * Default constructor. @@ -136,6 +140,7 @@ ToggleBoxConfig cfg = (ToggleBoxConfig) config; + format = cfg.format; checked = cfg.checked; mousePointer = cfg.mousePointer; } @@ -159,6 +164,7 @@ ClassNotFoundException { super.readExternal(in); + format = readString(in); checked = in.readBoolean(); mousePointer = readString(in); } @@ -179,6 +185,7 @@ throws IOException { super.writeExternal(out); + writeString(out, format); out.writeBoolean(checked); writeString(out, mousePointer); } === modified file 'src/com/goldencode/p2j/ui/ToggleBoxWidget.java' --- src/com/goldencode/p2j/ui/ToggleBoxWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/ToggleBoxWidget.java 2021-01-12 18:46:50 +0000 @@ -2,7 +2,7 @@ ** Module : ToggleBoxWidget.java ** Abstract : Toggle-box widget. ** -** Copyright (c) 2010-2020, Golden Code Development Corporation. +** Copyright (c) 2010-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 SVL 20101004 Created initial version. @@ -32,6 +32,11 @@ ** logic is assigning a widget attribute. ** 018 AIL 20191024 Fixed setChecked() to update the ScreenBuffer. ** 20200401 Fixed isChecked() and setChecked() for when the widget is not realized. +** 019 EVL 20201022 Optimized attributes flush implementation. +** CA 20201119 The DATA-TYPE for a dynamic TOGGLE-BOX must be LOGICAL. +** CA 20201126 The SCREEN-VALUE for a TOGGLE-BOX can be either 'yes' or 'no', never unknown. +** CA 20210111 SCREEN-VALUE usage from converted code needs to be distinguished from internal usage, +** within FWD code (see COMBO-BOX:LIST-ITEM-PAIRS behavior). */ /* ** This program is free software: you can redistribute it and/or modify @@ -115,16 +120,44 @@ public ToggleBoxWidget(boolean dynamic) { super(dynamic, new ToggleBoxConfig()); + + if (dynamic) + { + config.dataType = "logical"; + } } /** + * Internal worker for setting the SCREEN-VALUE on a per-widget basis. + * + * @param frameBuf + * The frame buffer where to save the value. + * @param value + * The value to be set via SCREEN-VALUE attribute. + * @param inUIStmt + * Flag indicating this call originates from a UI statement. + * + * @return true if the caller can proceed, as the screen-value can be set. + */ + @Override + boolean setScreenValue(ScreenBuffer frameBuf, Object value, boolean inUIStmt) + { + if (value instanceof BaseDataType && ((BaseDataType) value).isUnknown()) + { + ((BaseDataType) value).assign(new logical(false)); + } + + return super.setScreenValue(frameBuf, value, inUIStmt); + } + + /** * Gets the CHECKED writable attribute. * * @return The current value of the CHECKED attribute. */ public logical isChecked() { - if (!getAttr("realized", () -> config.realized)) + if (!getAttr("realized", () -> config.realized, true)) { // the toggle box can be checked even if not realized return new logical(getAttr("checked", () -> config.checked)); @@ -173,7 +206,7 @@ return; } - if (getAttr("realized", () -> config.realized)) + if (getAttr("realized", () -> config.realized, true)) { if (config.checked == checked.booleanValue()) { @@ -181,7 +214,7 @@ } setAttr("checked", config.checked, checked.booleanValue(), (vv) -> config.checked = vv); - setScreenValue(new character(checked)); + setScreenValueInt(new character(checked)); } else { @@ -199,7 +232,7 @@ @Override public boolean preRealizeCheck() { - double widthRequested = getAttr("widthChars", () -> config.widthChars); + double widthRequested = getAttr("widthChars", () -> config.widthChars, true); // safe, comparison of scaled doubles // this check is done only for ChUI mode - GUI toggle-box has no lower limit on the width === modified file 'src/com/goldencode/p2j/ui/TreeFace.java' --- src/com/goldencode/p2j/ui/TreeFace.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/TreeFace.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeFace.java ** Abstract : Common tree widget interface. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -23,6 +23,7 @@ ** startLabelEdit. ** 009 SBI 20200619 Added getNewNodeLabel() and setNewNodeLabel(character). ** 010 SBI 20200722 Added getHWND() to support controlFrame:TreeView:HWND conversion. +** 011 VVT 20210127 Fixed Javadoc for getSelectedNodeKey() to match original OCX */ /* ** This program is free software: you can redistribute it and/or modify @@ -204,7 +205,8 @@ /** * Getter for the SELECTED-NODE-KEY attribute. - * The method returns a key of the currently selected node or unknown when no node is currently + * The method returns keys of the currently selected nodes, delimited with the char(1), + * or an empty string when no node is currently * selected. * * @return the attribute value. === modified file 'src/com/goldencode/p2j/ui/TreeListConfig.java' --- src/com/goldencode/p2j/ui/TreeListConfig.java 2020-09-17 11:46:00 +0000 +++ src/com/goldencode/p2j/ui/TreeListConfig.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeListConfig.java ** Abstract : TreeList widget configuration. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -11,6 +11,9 @@ ** 003 IAS 20200823 Rework (de)serialization. ** 004 IAS 20200828 Fixed type parameters for generic classes. ** 005 IAS 20200908 Rework (de)serialization. +** EVL 20201015 Fixed missed showHeader flag from reading/writing/setting processes. +** VVT 20210127 TreeListConfig constructor: the 'visible' flag is now passed an an argument. +** VVT 20210128 the 'positions' field removed (the position info is stored iby columns now) */ /* ** This program is free software: you can redistribute it and/or modify @@ -85,12 +88,6 @@ public List columns = new ArrayList<>(); /** - * The columns order. The indexes correspond to the indexes of the columns list above. The values - * represent the visual order of the columns starting at 0 being the left-most column. - */ - public List positions = new ArrayList<>(); - - /** * 1-based column indexes (corresponding to the columns list above) of sorted columns, last entry is the * most significant. Positive value indicates ascending order, negative descending order. To get the * actual column index do Math.abs(value) - 1 @@ -183,9 +180,9 @@ TreeListConfig cfg = (TreeListConfig) config; columns = cfg.columns; - positions = cfg.positions; sort = cfg.sort; fixedColumn = cfg.fixedColumn; + showHeader = cfg.showHeader; thousandSeparator = cfg.thousandSeparator; decimalSeparator = cfg.decimalSeparator; dateSeparator = cfg.dateSeparator; @@ -222,9 +219,9 @@ { super.readExternal(in); columns = readList(in, newColumn()); - positions = readList(in, s -> readInteger(s)); sort = readList(in, s -> readInteger(s)); fixedColumn = in.readShort(); + showHeader = in.readBoolean(); thousandSeparator = in.readUTF(); decimalSeparator = in.readUTF(); dateSeparator = in.readUTF(); @@ -248,9 +245,9 @@ { super.writeExternal(out); writeList(out, columns); - writeList(out, (o, v) -> writeInteger(o, v), positions); writeList(out, (o, v) -> writeInteger(o, v), sort); out.writeShort(fixedColumn); + out.writeBoolean(showHeader); out.writeUTF(thousandSeparator); out.writeUTF(decimalSeparator); out.writeUTF(dateSeparator); @@ -341,14 +338,18 @@ public byte align; public int bgcolor; public int fgcolor; - public boolean visible = true; + public boolean visible; + + /** + * The column position, can only be changed at the client side. + */ + public int position; /** * Default ctor. */ public Column() { - } /** @@ -366,8 +367,19 @@ * Column background color. * @param fgcolor * Column foreground color. + * @param visible + * Column visibility flag. + * @param position + * the column position in visible column order */ - public Column(String caption, byte dataType, int width, byte align, int bgcolor, int fgcolor) + public Column(String caption, + byte dataType, + int width, + byte align, + int bgcolor, + int fgcolor, + boolean visible, + int position) { this.caption = caption; this.dataType = dataType; @@ -375,6 +387,32 @@ this.align = align; this.bgcolor = bgcolor; this.fgcolor = fgcolor; + this.visible = visible; + this.position = position; + } + + /** + * Ctor. Create a visible column. + * + * @param caption + * Column caption. + * @param dataType + * Data type. + * @param width + * Column width. + * @param align + * Horizontal value alignment. + * @param bgcolor + * Column background color. + * @param fgcolor + * Column foreground color. + * @param position + * the column position in visible column order + */ + public Column(String caption, byte dataType, int width, byte align, int bgcolor, + int fgcolor, int position) + { + this(caption, dataType, width, align, bgcolor, fgcolor, true, position); } /** @@ -398,7 +436,7 @@ out.writeInt(bgcolor); out.writeInt(fgcolor); out.writeBoolean(visible); - + out.writeInt(position); } /** @@ -424,8 +462,10 @@ bgcolor = in.readInt(); fgcolor = in.readInt(); visible = in.readBoolean(); + position = in.readInt(); } } + /** * Create node entry instance. * @return node entry instance === modified file 'src/com/goldencode/p2j/ui/TreeListNodeEntry.java' --- src/com/goldencode/p2j/ui/TreeListNodeEntry.java 2020-09-14 13:46:01 +0000 +++ src/com/goldencode/p2j/ui/TreeListNodeEntry.java 2021-01-29 14:13:08 +0000 @@ -2,13 +2,14 @@ ** Module : TreeListNodeEntry.java ** Abstract : Treelist configuration node entry. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. ** 002 HC 20190514 Implemented additional set of features for TREELIST widget and related ** changes. ** 003 IAS 20200908 Rework (de)serialization, Fixed type parameters for generic classes. +** 004 VVT 20210127 Added: equals() and hashCode(): all class fields must participate. */ /* ** This program is free software: you can redistribute it and/or modify @@ -174,4 +175,31 @@ super.readExternal(in); cellValues = readList(in, s -> readCell(s)); } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param o + * The other object to test. + * + * @return {@code true} if this object is equals to the o argument; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) + { + return super.equals(obj) && Objects.equals(cellValues, ((TreeListNodeEntry) obj).cellValues); + } + + /** + * Returns a hash code value for the object. This method is + * supported for the benefit of hash tables such as those provided by + * {@link java.util.HashMap}. + * + * @return a hash code value for this object. + */ + @Override + public int hashCode() + { + return 31 + ((cellValues == null) ? 0 : cellValues.hashCode()); + } } === modified file 'src/com/goldencode/p2j/ui/TreeListWidget.java' --- src/com/goldencode/p2j/ui/TreeListWidget.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/TreeListWidget.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeListWidget.java ** Abstract : TreeList widget. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -13,6 +13,16 @@ ** Unused private methods removed. ** 005 HC 20200313 Javadoc fixes. ** 006 IAS 20200913 Fixed type parameters for generic classes. +** HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. +** VVT 20210120 Missing support for column synchronization added. +** See #5084. +** VVT 20210127 clearAll() is now responsive for initial state recreation; the additional +** addColumn() helper method added to add columns created in subclasses; +** setColumnVisible(): attribute synchronization fixed. +** VVT 20210127 Fixed: missing client update in setColumnWidth() +** VVT 20210128 The local 'positions' field removed: column positions are never changed +** by the server. Method order changed to conform the FWD style. */ /* ** This program is free software: you can redistribute it and/or modify @@ -69,6 +79,9 @@ package com.goldencode.p2j.ui; +import java.util.*; + +import com.goldencode.p2j.ui.TreeListConfig.*; import com.goldencode.p2j.util.*; import com.google.common.base.Objects; @@ -81,6 +94,20 @@ extends TreeWidgetBase, TreeListNodeResource> implements TreeList { + /** Flag marking columns or column position changes are pending */ + protected boolean pendingPushColumns; + + /** + * List of columns. In BUILDING-TREE mode, the changes in column + * list are reflected in this field value only, and they are + * flushed to the client and config.columns when BUILD-TREE mode is + * switched off. + *

+ * WHen BUILDING-TREE mode of off, this list contains same entries + * as config.columns + */ + protected final List columns = new ArrayList<>(); + /** * Default constructor. */ @@ -99,16 +126,8 @@ { this(dynamic, new TreeListConfig()); - // create the first column for the tree view - TreeListConfig.Column col = new TreeListConfig.Column("", - TreeListConfig.Column.TYPE_CHAR, - 200, - TreeListConfig.Column.ALIGN_LEFT, - ColorTable._rgbValue(0, 0, 0), - ColorTable._rgbValue(255, 255, 255)); - col.visible = true; - config.columns.add(col); - config.positions.add(0); + clearAll(); + setAttr("sorted", config.sorted, true, (vv) -> config.sorted = vv); } @@ -160,7 +179,7 @@ else { int index = value.intValue(); - if (index >= 0 && index < getAttr("columns", () -> config.columns).size()) + if (index >= 0 && index < columns.size()) { getAttr("sort", () -> config.sort).remove(index); getAttr("sort", () -> config.sort).add(index); @@ -215,7 +234,7 @@ } /** - * Adds new column to the tree-list. + * Create a new column and append to the tree-list columns. * * @param caption * Column caption. @@ -256,24 +275,40 @@ return new integer(); } - String _caption = caption.toStringMessage(); - byte _dataType = (byte) dataType.intValue(); - int _width = width == null || width.isUnknown() ? -1 : width.intValue(); - byte _align = (byte) (align == null || align.isUnknown() ? -1 : align.intValue()); - int _bgcolor = (byte) (bgColor == null || bgColor.isUnknown() ? -1 : bgColor.intValue()); - int _fgcolor = (byte) (fgColor == null || fgColor.isUnknown() ? -1 : fgColor.intValue()); - - TreeListConfig.Column col = new TreeListConfig.Column(_caption, - _dataType, - _width, - _align, - _bgcolor, - _fgcolor); - getAttr("columns", () -> config.columns).add(col); - int i = getAttr("columns", () -> config.columns).size() - 1; - getAttr("positions", () -> config.positions).add(i); - - return new integer(i); + final int columnsSize = columns.size(); + + final Column col = new Column(caption.toStringMessage(), (byte) dataType.intValue(), + width == null || width.isUnknown() ? -1 : width.intValue(), + (byte) (align == null || align.isUnknown() ? -1 : align.intValue()), + (int) (byte) (bgColor == null || bgColor.isUnknown() ? -1 : bgColor.intValue()), + (int) (byte) (fgColor == null || fgColor.isUnknown() ? -1 : fgColor.intValue()), + columnsSize); + addColumn(col); + + return new integer(columnsSize); + } + + /** + * Setter for the BUILDING-TREE attribute. When set to {@code true} + * any model changes (columns added, nodes added or removed) won't + * be reflected until the attribute is set back to {@code false}. + * + * @param value + * the attribute value + */ + @Override + public void setBuildingTree(logical value) + { + super.setBuildingTree(value); + + if (buildingTree <= 0) + { + if (pendingPushColumns) + { + setAttr("columns", config.columns, columns, v -> config.columns = new ArrayList(v)); + pendingPushColumns = false; + } + } } /** @@ -287,12 +322,22 @@ @Override public void setColumnCaption(NumberType index, character caption) { - flushWidgetAttrs(); - TreeListConfig.Column col = config.getColumn(index); - if (col != null) - { - col.caption = caption.toStringMessage(); - } + final TreeListConfig.Column col = config.getColumn(index); + if (col == null) + { + LOG.warning("Invalid column index: " + index); + return; + } + + final String newCaption = caption.toStringMessage(); + + if (col.caption.equals(newCaption)) + { + return; + } + + columnUpdate(index.intValue(), new Column(newCaption, col.dataType, col.width, col.align, + col.bgcolor, col.fgcolor, col.visible, col.position)); } /** @@ -328,15 +373,46 @@ { if (!assertKnown(index, width)) { - return; - } - - flushWidgetAttrs(); - TreeListConfig.Column col = config.getColumn(index); - if (col != null) - { - col.width = width.intValue(); - } + /** + * FIXME: or, must throw an exception? + */ + return; + } + + final TreeListConfig.Column col = config.getColumn(index); + if (col == null) + { + LOG.warning("Invalid column index: " + index); + return; + } + + int newWidth = width.intValue(); + + if (newWidth > 46231 || newWidth < 0) + { + /** + * FIXME: an exception is thrown in original OCX + */ + LOG.warning("Invalid column width: " + index); + return; + } + + + /** + * Limit the column width by a minimum value. + */ + if (newWidth < 20) + { + newWidth = 20; + } + + if (col.width == newWidth) + { + return; + } + + columnUpdate(index.intValue(), new TreeListConfig.Column(col.caption, col.dataType, + newWidth, col.align, col.bgcolor, col.fgcolor, col.visible, col.position)); } /** @@ -356,12 +432,12 @@ } int i = index.intValue(); - if (i >= 0 && i < getAttr("columns", () -> config.columns).size()) + if (i < 0 || i >= columns.size()) { - return new integer(getAttr("positions", () -> config.positions).get(i)); + return new integer(); } - return new integer(); + return new integer(columns.get(i).position); } /** @@ -542,12 +618,20 @@ return; } - flushWidgetAttrs(); - TreeListConfig.Column col = config.getColumn(colIndex); - if (col != null) - { - col.visible = visible.booleanValue(); - } + final TreeListConfig.Column col = config.getColumn(colIndex); + if (col == null) + { + return; + } + + final boolean newVisible = visible.booleanValue(); + if(col.visible == newVisible) + { + return; + } + + columnUpdate(colIndex.intValue(), new TreeListConfig.Column(col.caption, col.dataType, + col.width, col.align, col.bgcolor, col.fgcolor, newVisible, col.position)); } /** @@ -770,6 +854,72 @@ } /** + * This methods calls super, then clear all columns. + */ + @Override + public void clearAll() + { + super.clearAll(); + + columns.clear(); + + // create the first column for the tree view + addColumn(new Column("", Column.TYPE_CHAR, 200, Column.ALIGN_LEFT, + ColorTable._rgbValue(0, 0, 0), ColorTable._rgbValue(255, 255, 255), 0)); + } + + /** + * The method is called after the configuration associated with + * the implementor changes. The parameter points to a reference + * representing the original configuration state. This reference + * can be used to detect what configuration fields changed and so + * optimize any processing related to the configuration change. + * + * @param beforeUpdate + * Config reference capturing the config state before + * the owner's config was modified. + */ + @Override + public void afterConfigUpdate(TreeListConfig beforeUpdate) + { + super.afterConfigUpdate(beforeUpdate); + + if (pendingPushColumns) + { + return; + } + + /** + * Copy columns for now. Note: we have no simple means currently + * to compare columns, since no proper equals() method is defined for columns. + */ + columns.clear(); + columns.addAll(config.columns); + } + + /** + * Append a newly-created column to the end of + * column list. + * + * @param column + * the column to append + */ + protected final void addColumn(Column column) + { + columns.add(column); + + if (buildingTree <= 0) + { + setAttr("columns", config.columns, columns, v -> config.columns.add(column)); + pendingPushColumns = false; + } + else + { + pendingPushColumns = true; + } + } + + /** * Creates new node. * * @param key @@ -790,6 +940,29 @@ } /** + * Update a column at the given index. + * + * @param index + * valid column index + * @param column + * the new column + */ + private void columnUpdate(final int index, final TreeListConfig.Column column) + { + columns.set(index, column); + + if (buildingTree <= 0) + { + setAttr("columns", config.columns, columns, v -> config.columns = new ArrayList(v)); + pendingPushColumns = false; + } + else + { + pendingPushColumns = true; + } + } + + /** * Finds column configuration entry from column index. * * @param colIndex @@ -818,12 +991,12 @@ */ private TreeListConfig.Column findColumn(int colIndex) { - if (colIndex < 0 || colIndex >= getAttr("columns", () -> config.columns).size()) + if (colIndex < 0 || colIndex >= columns.size()) { return null; } - return getAttr("columns", () -> config.columns).get(colIndex); + return columns.get(colIndex); } /** @@ -873,7 +1046,7 @@ int i = colIndex.intValue(); - if (i < 0 || i >= getAttr("columns", () -> config.columns).size()) + if (i < 0 || i >= columns.size()) { return; } === modified file 'src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java' --- src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java 2020-09-22 20:34:41 +0000 +++ src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java 2021-01-28 21:48:33 +0000 @@ -2,7 +2,7 @@ ** Module : TreeNodeCollectionResource.java ** Abstract : Tree nodes collection resource implementation. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -19,6 +19,9 @@ ** 20200529 Added new overload for add(...) with complete list of parameters. ** 006 IAS 20200914 Fixed type parameters for generic classes. ** SBI 20200922 Changed _addNodeImpl(...) to implement adding a previous node. +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. +** VVT 20210127 Node with non-unique node IDs can now be created, non-unique IDs recorded in the log. +** VVT 20210128 Method order changed to conform the FWD style. */ /* ** This program is free software: you can redistribute it and/or modify @@ -576,19 +579,22 @@ } /** - * Worker to be implemented by each resource. Called by {@link #delete()}. - * - * @return true if the resource was deleted. + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. */ @Override - protected boolean resourceDelete() + public void delete() { - if (tree == null) + if (!resourceDelete()) { - // already deleted - return false; + return; } - + + super.delete(); + TreeWidgetBase tree = this.tree; this.tree = null; @@ -602,9 +608,52 @@ } tree.pushNodes(); + } + + /** + * Returns the count of all the items in this collection. + * + * @return The number of all nodes in this collection + */ + @Override + public integer getCount() + { + ListIterator iter = iterator(); + + int collect = 0; + + while(iter.hasNext()) + { + R node = iter.next(); + + if (node._getHasChildren()) + { + collect += node._getNodes().getCount().intValue(); + } + + collect++; + } + + return new integer(collect); + } + + /** + * Worker to be implemented by each resource. Called by {@link #delete()}. + * + * @return true if the resource was deleted. + */ + @Override + protected boolean resourceDelete() + { + if (tree == null) + { + // already deleted + return false; + } + return true; } - + /** * Recursively removes the specified node from the collection and the tree. This will also delete the * resource. @@ -713,12 +762,6 @@ text = key; } - R node = tree.findNode(key.toStringMessage()); - if (node != null) - { - return new handle(); - } - R parent = owner; if (location == Location.CHILD) @@ -735,7 +778,8 @@ { nodes = new LinkedList<>(); } - node = createNode(key.toStringMessage(), text.toStringMessage(), parent); + + R node = createNode(key.toStringMessage(), text.toStringMessage(), parent); switch (location) { @@ -817,12 +861,6 @@ throw new IllegalArgumentException(); } - TreeNodeResource node = tree.findNode(key); - if (node != null) - { - throw new RuntimeException("Cannot create node, duplicate key found."); - } - return tree.createNode(key, text, parent); } @@ -976,31 +1014,4 @@ } } } - - /** - * Returns the count of all the items in this collection. - * - * @return The number of all nodes in this collection - */ - @Override - public integer getCount() - { - ListIterator iter = iterator(); - - int collect = 0; - - while(iter.hasNext()) - { - R node = iter.next(); - - if (node._getHasChildren()) - { - collect += node._getNodes().getCount().intValue(); - } - - collect++; - } - - return new integer(collect); - } } === modified file 'src/com/goldencode/p2j/ui/TreeNodeEntry.java' --- src/com/goldencode/p2j/ui/TreeNodeEntry.java 2020-09-14 12:05:37 +0000 +++ src/com/goldencode/p2j/ui/TreeNodeEntry.java 2021-01-28 21:48:33 +0000 @@ -1,8 +1,8 @@ /* -** Module : TreeVNodeEntry.java +** Module : TreeNodeEntry.java ** Abstract : Tree node configuration entry. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -12,6 +12,7 @@ ** its values from EditorMode. ** 005 IAS 20200828 Fixed type parameters for generic classes. ** 006 IAS 20200908 Rework (de)serialization. +** 007 VVT 20210127 Fixed: equals() and hashCode(): all class fields must participate. ** */ /* @@ -169,21 +170,33 @@ * @param o * The other object to test. * - * @return {@code true} if this object is the same as the o argument; {@code false} otherwise. + * @return {@code true} if this object is equals to the o argument; {@code false} otherwise. */ @Override - public boolean equals(Object o) + public boolean equals(Object obj) { - if (this == o) + if (this == obj) { return true; } - if (o == null || getClass() != o.getClass()) - { - return false; - } - TreeNodeEntry that = (TreeNodeEntry) o; - return Objects.equals(nodeId, that.nodeId); + + if (obj == null) + { + return false; + } + + if (getClass() != obj.getClass()) + { + return false; + } + + final TreeNodeEntry other = (TreeNodeEntry) obj; + return editorMode == other.editorMode && expandIconId == other.expandIconId + && expanded == other.expanded && hasChildren == other.hasChildren + && iconId == other.iconId && nodeBgColor == other.nodeBgColor + && nodeFgColor == other.nodeFgColor && nodeId == other.nodeId + && parentId == other.parentId && selectedIconId == other.selectedIconId + && Objects.equals(tooltip, other.tooltip); } /** @@ -196,7 +209,18 @@ @Override public int hashCode() { - return Objects.hash(nodeId); + final int prime = 31; + int result = prime + editorMode.hashCode(); + result = prime * result + expandIconId; + result = prime * result + (expanded ? 1231 : 1237); + result = prime * result + (hasChildren ? 1231 : 1237); + result = prime * result + iconId; + result = prime * result + nodeBgColor; + result = prime * result + nodeFgColor; + result = prime * result + nodeId; + result = prime * result + parentId; + result = prime * result + selectedIconId; + return prime * result + ((tooltip == null) ? 0 : tooltip.hashCode()); } /** === modified file 'src/com/goldencode/p2j/ui/TreeNodeResource.java' --- src/com/goldencode/p2j/ui/TreeNodeResource.java 2020-09-14 12:05:37 +0000 +++ src/com/goldencode/p2j/ui/TreeNodeResource.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeNodeResource.java ** Abstract : Tree node resource implementation. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -25,6 +25,9 @@ ** enumeration type EditorMode. ** 009 SBI 20200721 Implemented CHILD, NEXT, PREVIOUS, CHILDREN, FirstSibling, LastSibling attributes. ** 010 IAS 20200914 Fixed type parameters for generic classes. +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. +** VVT 20210127 getTreeNodeIndex(): implementation updated to reflect the root node existance. +** VVT 20210128 Method order changed to conform the FWD style. */ /* ** This program is free software: you can redistribute it and/or modify @@ -737,7 +740,7 @@ return new integer(); } - return new integer(index + 1); + return new integer(index); } /** @@ -893,6 +896,38 @@ } /** + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + super.delete(); + + TreeWidgetBase, TreeNodeResource> tree = this.tree; + this.tree = null; + + if (nodeId == 0) + { + _removeCollection(); + } + else + { + parent._getNodes()._removeNode(this, null); + } + + tree.removeNode(this, true); + } + + /** * Worker to be implemented by each resource. Called by {@link #delete()}. * * @return true if the resource was deleted. @@ -905,24 +940,50 @@ // already deleted) return false; } - - TreeWidgetBase, TreeNodeResource> tree = this.tree; - this.tree = null; - - if (nodeId == 0) - { - _removeCollection(); - } - else - { - parent._getNodes()._removeNode(this, null); - } - - tree.removeNode(this, true); + return true; } /** + * The method serializes the node state into new config entry instance. + * + * @return new instance of {@link TreeNodeEntry}. + */ + protected abstract N saveToEntry(); + + /** + * The method serializes the node state into the supplied entry instance. + * + * @param entry + * Instance of {@link TreeNodeEntry}. + */ + protected void saveToEntry(TreeNodeEntry entry) + { + entry.expanded = expanded; + entry.nodeBgColor = bgColor; + entry.nodeFgColor = fgColor; + entry.iconId = iconId; + entry.expandIconId = expandIconId; + entry.hasChildren = hasChildren; + entry.tooltip = tooltip; + entry.selectedIconId = selectedIconId; + entry.editorMode = editorMode; + } + + /** + * Loads the state from the supplied instance of {@link TreeNodeEntry}. + * + * @param entry + * A valid {@link TreeNodeEntry} instance. + */ + protected void loadFromEntry(N entry) + { + expanded = entry.expanded; + + editorMode = entry.editorMode; + } + + /** * Returns the collection of child nodes. * * @return see above. @@ -990,26 +1051,6 @@ } /** - * The method serializes the node state into new config entry instance. - * - * @return new instance of {@link TreeNodeEntry}. - */ - protected abstract N saveToEntry(); - - /** - * Loads the state from the supplied instance of {@link TreeNodeEntry}. - * - * @param entry - * A valid {@link TreeNodeEntry} instance. - */ - protected void loadFromEntry(N entry) - { - expanded = entry.expanded; - - editorMode = entry.editorMode; - } - - /** * Returns the node's parent. * * @return parent node. @@ -1130,22 +1171,4 @@ tree.pushNodes(); } - /** - * The method serializes the node state into the supplied entry instance. - * - * @param entry - * Instance of {@link TreeNodeEntry}. - */ - protected void saveToEntry(TreeNodeEntry entry) - { - entry.expanded = expanded; - entry.nodeBgColor = bgColor; - entry.nodeFgColor = fgColor; - entry.iconId = iconId; - entry.expandIconId = expandIconId; - entry.hasChildren = hasChildren; - entry.tooltip = tooltip; - entry.selectedIconId = selectedIconId; - entry.editorMode = editorMode; - } } === modified file 'src/com/goldencode/p2j/ui/TreeViewNodeEntry.java' --- src/com/goldencode/p2j/ui/TreeViewNodeEntry.java 2020-09-14 12:05:37 +0000 +++ src/com/goldencode/p2j/ui/TreeViewNodeEntry.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeViewNodeEntry.java ** Abstract : Treeview node configuration. ** -** Copyright (c) 2018-2020, Golden Code Development Corporation. +** Copyright (c) 2018-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20181122 Created initial version. @@ -10,6 +10,7 @@ ** it the base for the TREELIST widget. ** 003 SBI 20200619 Implemented setValue(int, Object). ** 004 IAS 20200908 Rework (de)serialization. +** 005 VVT 20210127 Added: equals() and hashCode(): all class fields must participate. */ /* ** This program is free software: you can redistribute it and/or modify @@ -68,6 +69,7 @@ import static com.goldencode.util.NativeTypeSerializer.*; import java.io.*; +import java.util.*; /** * Treeview node configuration. @@ -174,4 +176,32 @@ super.readExternal(in); text = readString(in); } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param o + * The other object to test. + * + * @return {@code true} if this object is equals to the o argument; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) + { + return super.equals(obj) && Objects.equals(text, ((TreeViewNodeEntry) obj).text); + } + + /** + * Returns a hash code value for the object. This method is + * supported for the benefit of hash tables such as those provided by + * {@link java.util.HashMap}. + * + * @return a hash code value for this object. + */ + @Override + public int hashCode() + { + return 31 + ((text == null) ? 0 : text.hashCode()); + } + } \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/TreeWidgetBase.java' (properties changed: -x to +x) --- src/com/goldencode/p2j/ui/TreeWidgetBase.java 2020-09-28 02:06:35 +0000 +++ src/com/goldencode/p2j/ui/TreeWidgetBase.java 2021-01-29 14:13:08 +0000 @@ -2,7 +2,7 @@ ** Module : TreeWidgetBase.java ** Abstract : Tree widget base implementation. ** -** Copyright (c) 2019-2020, Golden Code Development Corporation. +** Copyright (c) 2019-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 HC 20190313 Created initial version. @@ -33,6 +33,23 @@ ** 011 SBI 20200721 Drop highlight node's functionality is different from the selected node logic. ** 012 IAS 20200914 Fixed type parameters for generic classes. ** HC 20200926 Performance optimizations of server-client config state synchronization. +** HC 20201010 Implemented selective config flushing. +** EVL 20201021 More optimization for getAttr() usage or widget ID getting. +** EVL 20201022 Optimized attributes flush implementation. +** CA 20201117 Do not use resourceDelete to delete the widget - use the delete() API for this code. +** VVT 20210120 Missing support for column synchronization added. +** See #5084. +** AIL 20210126 Added support for selection changing after add/remove/collapse node. +** VVT 20210127 Node creation with non-unique IDs is now logged; the tree root node is now +** registered in nodesById, nodesByKey and nodesByIndices; getSelectedNode(): javadoc +** fixed to better match the original OCX; new private helper methods added: +** _getSelecteNodeId() and _getSelectedNode(); removeNode(): the doSelectAfterRemove() +** method inlined, the logic simplified. clearAll(): unnecessary call to pushNodes() +** removed. addNode(): now allows adding nodes with non-uniq IDs. See #5084. +** VVT 20210127 Logger access modifier changed to protected to make it accessible in subclasses. +** VVT 20210128 Method order changed to conform the FWD style. +** VVT 20210129 removeNode(): fixed for the case the selected node descends from the +** node being removed. */ /* ** This program is free software: you can redistribute it and/or modify @@ -89,13 +106,14 @@ package com.goldencode.p2j.ui; +import java.util.*; +import java.util.function.*; +import java.util.logging.*; + import com.goldencode.p2j.ui.TreeNodeEntry.*; import com.goldencode.p2j.ui.ocx.*; import com.goldencode.p2j.util.*; -import java.util.*; -import java.util.function.*; - /** * Server side Tree widget base implementation. * @param @@ -139,7 +157,7 @@ /** * The backed field for BUILDING-TREE attribute. */ - private int buildingTree = 0; + protected int buildingTree = 0; /** Flag marking pushNodes call pending */ private boolean pendingPushNodes; @@ -156,6 +174,9 @@ /** Assigned imagelist. */ private handle imageList; + /** Logger */ + protected static final Logger LOG = LogHelper.getLogger(TreeWidgetBase.class.getName()); + /** * Default constructor. * @@ -168,6 +189,9 @@ { super(dynamic, ctrl); rootNode = createNode("", "", null); + nodesById.put(Integer.valueOf(0), rootNode); + nodesByKey.put("", rootNode); + nodesByIndices.add(rootNode); resetImageMap(); } @@ -339,16 +363,15 @@ /** * Getter for the SELECTED-NODE attribute. - * The method returns a handle the currently selected node or unknown handle when no node is currently - * selected. The handle will wrap an instance of {@link TreeNodeFace}. + * The method returns a handle the currently selected node or tree root node handle when no node + * is currently selected. The handle will wrap an instance of {@link TreeNodeFace}. * * @return the attribute value. */ @Override public handle getSelectedNode() { - TreeNodeResource node = nodesById.get(getAttr("selectedNodeId", () -> config.selectedNodeId)); - return new handle(node); + return new handle(_getSelectedNode()); } /** @@ -361,44 +384,29 @@ @Override public void setSelectedNode(handle value) { - int nodeId = getNodeIdFrom(value); - - if (!nodesById.containsKey(nodeId)) - { - return; - } - - if (config.selectedNodeId == nodeId) - { - return; - } - - setAttr("selectedNodeId", config.selectedNodeId, nodeId, - (int vv) -> config.selectedNodeId = vv); + _setSelectedNode(getNodeIdFrom(value)); } /** * Getter for the SELECTED-NODE-ID attribute. * The method returns node id of the currently selected node or unknown when no node is currently * selected. + * + * FIXME: indeed this method never returns an unknown value. Instead the zero value is returned if no + * visible node is selected. * * @return the attribute value. */ @Override public integer getSelectedNodeId() { - handle node = getSelectedNode(); - if (node.isUnknown()) - { - return new integer(); - } - - return ((TreeNodeResource) node.getResource()).getNodeId(); + return new integer(_getSelecteNodeId()); } /** * Getter for the SELECTED-NODE-KEY attribute. - * The method returns a key of the currently selected node or unknown when no node is currently + * The method returns keys of the currently selected nodes, delimited with the char(1), + * or an empty string when no node is currently * selected. * * @return the attribute value. @@ -406,13 +414,7 @@ @Override public character getSelectedNodeKey() { - handle node = getSelectedNode(); - if (node.isUnknown()) - { - return new character(); - } - - return ((TreeNodeResource) node.getResource()).getNodeKey(); + return _getSelectedNode().getNodeKey(); } /** @@ -575,7 +577,7 @@ @Override public integer getFirstVisibleNode() { - return new integer(getAttr("topNode", () -> config.topNode)); + return new integer(getAttr("topNode", () -> config.topNode, true)); } /** @@ -901,16 +903,58 @@ @Override public void removeNode(NumberType nodeId) { - doWithNode(nodeId, null, n -> - { - TreeNodeResource parent = n._getParent(); - if (parent != null) + doWithNode(nodeId, null, n -> { + final TreeNodeResource parent = n._getParent(); + // We are not going to delete the tree root! + assert parent != null; + + final TreeNodeCollectionResource children = parent._getNodes(); + + /** + * If the selected node is being deleted or the selected node descends from + * the node being deleted, adjust the selection. + */ + for (R node = _getSelectedNode(); node != rootNode; node = (R) node._getParent()) { - TreeNodeCollectionResource nodes = parent._getNodes(); - nodes._removeNode(n, null); - parent._setHasChildren(nodes._getCount() != 0); - pushNodes(); + if (node == n) + { + // We are about to delete the focused node, transfer focus to: + // 1. The previous sibling, if exists + // 2. The parent, if it is not the tree root node + // 3. The next sibling, if exists + // 4. The tree root node + + final int nodeChildIndex = children._indexOf(n); + assert nodeChildIndex >= 0; + + final TreeNodeResource newFocused; + if (nodeChildIndex > 0) + { + newFocused = children._getNode(nodeChildIndex - 1); + } + else if (parent != rootNode) + { + newFocused = parent; + } + else if (nodeChildIndex < children._getCount() - 1) + { + newFocused = children._getNode(nodeChildIndex + 1); + } + else + { + newFocused = rootNode; + } + + _setSelectedNode(newFocused._getNodeId()); + + break; + } } + + children._removeNode(n, null); + parent._setHasChildren(children._getCount() != 0); + + pushNodes(); }); } @@ -949,7 +993,11 @@ @Override public void collapseNode(NumberType nodeId) { - doWithNode(nodeId, null, n -> n.setNodeExpanded(new logical(false))); + doWithNode(nodeId, null, n -> + { + n.setNodeExpanded(new logical(false)); + doSelectAfterCollapse(n); + }); } /** @@ -959,7 +1007,6 @@ public void clearAll() { rootNode._removeCollection(); - pushNodes(); } /** @@ -1245,8 +1292,9 @@ } /** - * Setter for the BUILDING-TREE attribute. When set to {@code true} any model changes (nodes added or - * removed) won't be reflected until the attribute is set back to {@code false}. + * Setter for the BUILDING-TREE attribute. When set to {@code true} + * any model changes (columns added, nodes added or removed) won't + * be reflected until the attribute is set back to {@code false}. * * @param value * the attribute value @@ -1272,6 +1320,7 @@ if (buildingTree <= 0) { buildingTree = 0; + if (pendingPushNodes) { pushNodes(); @@ -1354,12 +1403,12 @@ return unknownKey; } - if (getAttr("id ", () -> config.id )!= null) + int wid = getId(); + if (wid != -1) { Integer nodeId; - nodeId = LogicalTerminal.getClient().findTreeNodeByRelativeCoordinates(getAttr("id", () -> config.id).asInt(), - x.intValue(), + nodeId = LogicalTerminal.getClient().findTreeNodeByRelativeCoordinates(wid, x.intValue(), y.intValue()); if (nodeId != null) { @@ -1640,7 +1689,7 @@ @Override public integer getTopVisibleNodeId() { - return new integer(getAttr("topNode", () -> config.topNode)); + return new integer(getAttr("topNode", () -> config.topNode, true)); } /** @@ -1863,21 +1912,145 @@ } /** - * Delete the resource. - * - * @return true if the resource was deleted. - */ - @Override - protected boolean resourceDelete() - { - boolean res = super.resourceDelete(); - if (res) - { - rootNode.delete(); - return true; - } - - return false; + * Perform actual delete of an resource. At the time of this call, it is assumed the resource + * is valid for deletion (the handle and the resource are both valid). + *

+ * The method first calls {@link #resourceDelete()}, if the method returns {@code false}, the deletion + * is aborted. + */ + @Override + public void delete() + { + if (!resourceDelete()) + { + return; + } + + super.delete(); + + rootNode.delete(); + } + + /** + * Push the specified widget attribute change to the client-side. + * + * @param fname + * The config field name. + * @param value + * The new config field value. + */ + @Override + public void pushWidgetAttr(String fname, Object value) + { + if (buildingTree > 0) + { + pendingAttrs.add(fname); + pendingAttrValues.add(value); + } + else + { + super.pushWidgetAttr(fname, value); + } + } + + /** + * Push the specified widget attribute changes to the client-side. + * + * @param fnames + * The list of config field names. + * @param values + * The list of new config field values. + */ + @Override + public void pushWidgetAttr(String[] fnames, Object[] values) + { + if (buildingTree > 0) + { + pendingAttrs.addAll(Arrays.asList(fnames)); + pendingAttrValues.addAll(Arrays.asList(values)); + } + else + { + super.pushWidgetAttr(fnames, values); + } + } + + /** + * Get the TreeView node under mouse cursor as a node handle. + * If no node under cursor return unknown value. + * + * @param x + * Mouse cursor x position. + * @param y + * Mouse cursor y position. + * + * @return TreeView node under cursor as a handle. + */ + public handle hitTestFwd(NumberType x, NumberType y) + { + if (x.isUnknown() || y.isUnknown()) + { + return new handle(); + } + + R node = getNodeByCoordinates(x.intValue(), y.intValue()); + + return node == null ? new handle() : new handle(node); + } + + /** + * Get the TreeView node under mouse cursor as a COM object. + * If no node under cursor return unknown value. + * + * @param x + * Mouse cursor x position. + * @param y + * Mouse cursor y position. + * + * @return TreeView node under cursor as a COM object. + */ + public comhandle hitTest(NumberType x, NumberType y) + { + if (x.isUnknown() || y.isUnknown()) + { + return new comhandle(); + } + + R node = getNodeByCoordinates(x.intValue(), y.intValue()); + + if (node instanceof TreeViewNodeResource) + { + return TreeViewNodeObject.newInstance((TreeViewNodeResource) node); + } + else + { + return new comhandle(); + } + } + + /** + * Returns row height for tree based widget. The units are pixels. + * + * @return the tree widget row height in pixels. + */ + @Override + public integer getRowHeight() + { + integer fntSize = getFontSize(); + + return fntSize.isUnknown() ? new integer() : new integer(fntSize.intValue() + 1); + } + + /** + * Sets row height for tree based widget. The units are pixels. + * + * @param heightValue + * the new row height vale in pixels. + */ + @Override + public void setRowHeight(NumberType heightValue) + { + // stub only } /** @@ -1890,20 +2063,39 @@ */ protected void addNode(R node, boolean pushNodes) { - if (node.getNodeId().isUnknown()) + final integer nodeId = node.getNodeId(); + if (nodeId.isUnknown()) { throw new RuntimeException("Node id value must not be unknown!"); } - if (node.getNodeKey().isUnknown()) + final character nodeKey = node.getNodeKey(); + if (nodeKey.isUnknown()) { throw new RuntimeException("Node key value must not be unknown!"); } - nodesById.put(node.getNodeId().intValue(), node); - nodesByKey.put(node.getNodeKey().toStringMessage(), node); + nodesById.put(nodeId.intValue(), node); + + /** + * Note: to match the original OCX behavior, we allow adding nodes with existing keys. + */ + final String nodeKeyString = nodeKey.toStringMessage(); + if (nodesByKey.containsKey(nodeKeyString)) + { + LOG.warning("Creating node with duplicate key: " + nodeKeyString); + } + + nodesByKey.put(nodeKeyString, node); + nodesByIndices.add(node); + // this is the first node added to the tree-list; select it + if (node._getParent() == rootNode && rootNode._getNodes()._getCount() == 1) + { + _setSelectedNode(node._getNodeId()); + } + if (pushNodes) { pushNodes(); @@ -2094,6 +2286,28 @@ { return nodesByIndices.indexOf(node); } + + /** + * Convenient method for setting the selected node just by its id. + * + * @param nodeId + * The id of the tree node which should be selected. + */ + void _setSelectedNode(int nodeId) + { + if (!nodesById.containsKey(nodeId)) + { + return; + } + + if (config.selectedNodeId == nodeId) + { + return; + } + + setAttr("selectedNodeId", config.selectedNodeId, nodeId, + (int vv) -> config.selectedNodeId = vv); + } /** * Validates the supplied array of mandatory arguments and node id, finds the node nodeId and executes @@ -2283,51 +2497,6 @@ } /** - * Expands or collapses all nodes. - * - * @param expanded - * The new state. - * @param exceptNode - * The node to exclude from collapsing/expanding, if null all nodes will be collapsed/expanded. - */ - private void setAllNodesExpanded(boolean expanded, TreeNodeResource exceptNode) - { - TreeNodeCollectionResource nodes = rootNode._getNodes(false); - if (nodes != null) - { - Iterator it = nodes.recursiveIterator(false); - while(it.hasNext()) - { - TreeNodeResource node = it.next(); - if (node._isNodeExpanded() == expanded) - { - continue; - } - - TreeNodeResource check = exceptNode; - while(check != null) - { - if (check._getNodeId() == node._getNodeId()) - { - break; - } - - check = check._getParent(); - } - - if (check != null) - { - continue; - } - - node._setNodeExpanded(expanded); - } - - pushNodes(); - } - } - - /** * Removes all collapsed nodes. */ void clearCollapsed() @@ -2458,125 +2627,68 @@ // } /** - * Push the specified widget attribute change to the client-side. - * - * @param fname - * The config field name. - * @param value - * The new config field value. - */ - @Override - public void pushWidgetAttr(String fname, Object value) - { - if (buildingTree > 0) - { - pendingAttrs.add(fname); - pendingAttrValues.add(value); - } - else - { - super.pushWidgetAttr(fname, value); - } - } - - /** - * Push the specified widget attribute changes to the client-side. - * - * @param fnames - * The list of config field names. - * @param values - * The list of new config field values. - */ - @Override - public void pushWidgetAttr(String[] fnames, Object[] values) - { - if (buildingTree > 0) - { - pendingAttrs.addAll(Arrays.asList(fnames)); - pendingAttrValues.addAll(Arrays.asList(values)); - } - else - { - super.pushWidgetAttr(fnames, values); - } - } - - /** - * Get the TreeView node under mouse cursor as a node handle. - * If no node under cursor return unknown value. - * - * @param x - * Mouse cursor x position. - * @param y - * Mouse cursor y position. - * - * @return TreeView node under cursor as a handle. - */ - public handle hitTestFwd(NumberType x, NumberType y) - { - if (x.isUnknown() || y.isUnknown()) - { - return new handle(); - } - - R node = getNodeByCoordinates(x.intValue(), y.intValue()); - - return node == null ? new handle() : new handle(node); - } - - /** - * Get the TreeView node under mouse cursor as a COM object. - * If no node under cursor return unknown value. - * - * @param x - * Mouse cursor x position. - * @param y - * Mouse cursor y position. - * - * @return TreeView node under cursor as a COM object. - */ - public comhandle hitTest(NumberType x, NumberType y) - { - if (x.isUnknown() || y.isUnknown()) - { - return new comhandle(); - } - - R node = getNodeByCoordinates(x.intValue(), y.intValue()); - - if (node instanceof TreeViewNodeResource) - { - return TreeViewNodeObject.newInstance((TreeViewNodeResource) node); - } - else - { - return new comhandle(); - } - } - - /** - * Returns row height for tree based widget. The units are pixels. - * - * @return the tree widget row height in pixels. - */ - @Override - public integer getRowHeight() - { - integer fntSize = getFontSize(); - - return fntSize.isUnknown() ? new integer() : new integer(fntSize.intValue() + 1); - } - - /** - * Sets row height for tree based widget. The units are pixels. - * - * @param heightValue - * the new row height vale in pixels. - */ - @Override - public void setRowHeight(NumberType heightValue) - { - // stub only + * Expands or collapses all nodes. + * + * @param expanded + * The new state. + * @param exceptNode + * The node to exclude from collapsing/expanding, if null all nodes will be collapsed/expanded. + */ + private void setAllNodesExpanded(boolean expanded, TreeNodeResource exceptNode) + { + TreeNodeCollectionResource nodes = rootNode._getNodes(false); + if (nodes != null) + { + Iterator it = nodes.recursiveIterator(false); + while(it.hasNext()) + { + TreeNodeResource node = it.next(); + if (node._isNodeExpanded() == expanded) + { + continue; + } + + TreeNodeResource check = exceptNode; + while(check != null) + { + if (check._getNodeId() == node._getNodeId()) + { + break; + } + + check = check._getParent(); + } + + if (check != null) + { + continue; + } + + node._setNodeExpanded(expanded); + } + + pushNodes(); + } + } + + /** + * Get selected node by node ID helper method. + * + * @return the selected node + */ + private final R _getSelectedNode() + { + return nodesById.get(_getSelecteNodeId()); + } + + /** + * Get selected node ID helper method. + * + * @return see above + */ + private int _getSelecteNodeId() + { + return getAttr("selectedNodeId", () -> config.selectedNodeId, true); } /** @@ -2601,9 +2713,10 @@ { Integer nodeId = null; - if (getAttr("id ", () -> config.id )!= null) + int wid = getId(); + if (wid != -1) { - nodeId = LogicalTerminal.getClient().hitTest(getAttr("id", () -> config.id).asInt(), x, y); + nodeId = LogicalTerminal.getClient().hitTest(wid, x, y); } if (nodeId == null) @@ -2635,4 +2748,48 @@ return nodeId; } + + /** + * Change selection after a collapse operation. This should be triggered after a collapse so + * we ensure that if the selected node was part of the collapsed subtree, the new selection + * should be changed to the nearest visible ancestor (the collapsed node). + * + * @param collapsedNode + * The node on which the collapse operation was done. + */ + private void doSelectAfterCollapse(TreeNodeResource collapsedNode) + { + TreeNodeResource selectedAncestor = ((TreeNodeResource) getSelectedNode().getResource()); + + if (!validSelection() || collapsedNode == selectedAncestor) + { + return; + } + + while (selectedAncestor != null && + selectedAncestor != collapsedNode && + selectedAncestor != rootNode) + { + selectedAncestor = selectedAncestor._getParent(); + } + + + if (selectedAncestor == collapsedNode) + { + _setSelectedNode(collapsedNode._getNodeId()); + } + } + + /** + * Check if the current selected node is valid: is known and attached to this tree. + * + * @return {@true} if the selected node is known and belongs to this tree. + */ + private boolean validSelection() + { + integer selectedNode = getSelectedNodeId(); + return selectedNode != null && + !selectedNode.isUnknown() && + findNode(selectedNode.intValue()) != null; + } } === modified file 'src/com/goldencode/p2j/ui/TriggerManager.java' --- src/com/goldencode/p2j/ui/TriggerManager.java 2020-09-30 22:59:47 +0000 +++ src/com/goldencode/p2j/ui/TriggerManager.java 2020-10-15 07:54:57 +0000 @@ -50,6 +50,10 @@ ** 021 CA 20200927 Avoid context-local lookups by relying on the helper instance. ** GES 20200927 Added helper functions for dumping state. ** GES 20200930 Changed mergeTriggerRegistry() to use gather() instead of merge(). +** CA 20201011 Replaced integer sets with a fast access bitmap. +** Improved trigger cleanup - each resource knows the number of events it has +** registered with (ever), and if this number is zero, there is no work to do when +** the resource gets deleted (including widgets). */ /* @@ -110,6 +114,8 @@ import java.util.*; import java.util.stream.*; +import org.roaringbitmap.*; + import com.goldencode.p2j.util.*; /** @@ -148,21 +154,21 @@ *

* All non-persistent triggers registered in the scope are deregistered. *

- * All persistent triggers registered in this scope are moved to the - * previous scope. + * All persistent triggers registered in this scope are moved to the previous scope. * - * @param registry - * ScopedDictionary that keeps track of trigger - * registrations for this session. + * @param registry + * ScopedDictionary that keeps track of trigger registrations for this session. + * + * @return true if there were any events in the current scope. */ @SuppressWarnings("unchecked") - public static void scopeFinished(ScopedDictionary registry) + public static boolean scopeFinished(ScopedDictionary registry) { LinkedList list = (LinkedList) registry.getScope(); registry.deleteScope(); LinkedList prev = (LinkedList) registry.getScope(); - scopeFinished(list, prev, true); + return scopeFinished(list, prev, true); } /** @@ -189,7 +195,28 @@ // from the registry only and only if its creator procedure (or resource) is deleted LinkedList list = (LinkedList) registry.getScopeAt(0); list.addFirst(events); - + LogicalTerminal lt = LogicalTerminal.getInstance(); + events.processDefinitions((EventDefinition ed) -> + { + if (ed.hasWidget()) + { + ed.processWidgets((Integer wid) -> + { + lt.getWidgetForIdInt(wid).incrementTrigger(); + }); + } + if (ed.hasResource()) + { + ed.processResources((Long resid) -> + { + WrappedResource res = handle.fromResourceId(resid).getResource(); + if (res.valid() && res instanceof HandleResource) + { + ((HandleResource) res).incrementTrigger(); + } + }); + } + }); // DEBUG: System.err.printf("REGISTER\n%s", events.toString()); } @@ -475,22 +502,24 @@ * @param cleanup * Flag indicating if the triggers are cleaned up using * {@link #cleanup(EventList, Set)}. + * + * @return true if there were any events in the current scope. */ - private static void scopeFinished(LinkedList source, - LinkedList target, - boolean cleanup) + private static boolean scopeFinished(LinkedList source, + LinkedList target, + boolean cleanup) { // DEBUG: System.err.printf("SCOPE_FINISHED\n%s\n%s\n", dumpScope(source, "SOURCE"), dumpScope(target, "BEFORE_TARGET")); if (source.size() == 0) { - return; + return false; } LogicalTerminal lt = LogicalTerminal.getInstance(); ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper(); - Set wids = new HashSet(1); + RoaringBitmap wids = new RoaringBitmap(); for (EventList events : source) { @@ -525,7 +554,9 @@ clearRowDisplay(wids); } - // DEBUG: System.err.print(dumpScope(target, "AFTER_TARGET")); + // DEBUG: System.err.print(dumpScope(target, "AFTER_TARGET")); + + return true; } /** @@ -543,17 +574,17 @@ * This will be modified on output to add any widgets that need * to be removed from the notification list. */ - private static void cleanup(LogicalTerminal lt, EventList list, Set triggers) + private static void cleanup(LogicalTerminal lt, EventList list, RoaringBitmap triggers) { lt.deregisterTrigger(list.getTriggerId(), true); - Map rde = list.getRowDisplayEvents(); + int[][] rde = list.getRowDisplayEvents(); if (rde != null) { - for (Map.Entry entry : rde.entrySet()) + for (int i = 0; i < rde[1].length; i++) { - triggers.add(entry.getValue()); + triggers.add(rde[1][i]); } } } @@ -565,12 +596,11 @@ * @param triggers * The list of widgets no longer needing row display notifications. */ - private static void clearRowDisplay(Set triggers) + private static void clearRowDisplay(RoaringBitmap triggers) { - if (triggers.size() > 0) + if (!triggers.isEmpty()) { - int[] triggerIds = Utils.integerCollectionToPrimitive(triggers); - LogicalTerminal.removeRowDisplayEvents(triggerIds); + LogicalTerminal.removeRowDisplayEvents(triggers.toArray()); } } } === modified file 'src/com/goldencode/p2j/ui/TriggerMatch.java' --- src/com/goldencode/p2j/ui/TriggerMatch.java 2020-09-17 11:46:00 +0000 +++ src/com/goldencode/p2j/ui/TriggerMatch.java 2020-10-11 16:07:42 +0000 @@ -9,6 +9,7 @@ ** 002 HC 20191019 Improved the logic preventing recursive triggers to consider the cases when ** the widget matching the trigger is different from the source widget. ** 003 IAS 20200914 Re-work (de)serialization +** CA 20201011 Added a copy constructor. */ /* ** This program is free software: you can redistribute it and/or modify @@ -103,6 +104,19 @@ } /** + * Initialize this instance as a copy of the other instance. + * + * @param other + * The other instance. + */ + public TriggerMatch(TriggerMatch other) + { + this.eventId = other.eventId; + this.triggerId = other.triggerId; + this.matchedWidgetId = other.matchedWidgetId; + } + + /** * Replacement for the default object writing method. * * @param out === modified file 'src/com/goldencode/p2j/ui/WindowWidget.java' --- src/com/goldencode/p2j/ui/WindowWidget.java 2020-10-07 18:32:15 +0000 +++ src/com/goldencode/p2j/ui/WindowWidget.java 2021-01-15 20:55:46 +0000 @@ -114,6 +114,10 @@ ** 065 HC 20200926 Performance optimizations of server-client config state ** synchronization. ** AIL 20201007 Override canPushWidgetAttr method in order to define custom constraints. +** HC 20201010 Implemented selective config flushing. +** EVL 20201022 Optimized attributes flush implementation. +** SVL 20201221 The widget now implements MinHeightCharsInterface. +** VVT 20210115 Missing LegacyAttribute annotation(s) added. See #5064. */ /* ** This program is free software: you can redistribute it and/or modify @@ -188,11 +192,12 @@ implements CommonWindow, CommonHandleTree, TopOnlyInterface, - Hoverable + Hoverable, + MinHeightCharsInterface { /** MenuWidget handle owned by this window */ private MenuWidget menubar; - + /** * Default constructor. It is package private because an instance must * only be created in a controllable way. @@ -277,7 +282,7 @@ return new decimal(); } - return new decimal(getAttr("fullHeightChars", () -> config.fullHeightChars)); + return new decimal(getAttr("fullHeightChars", () -> config.fullHeightChars, true)); } /** @@ -293,7 +298,7 @@ return new integer(); } - return new integer(getAttr("fullHeightPixels", () -> config.fullHeightPixels)); + return new integer(getAttr("fullHeightPixels", () -> config.fullHeightPixels, true)); } /** @@ -309,7 +314,7 @@ return new decimal(); } - return new decimal(getAttr("fullWidthChars", () -> config.fullWidthChars)); + return new decimal(getAttr("fullWidthChars", () -> config.fullWidthChars, true)); } /** @@ -325,7 +330,7 @@ return new integer(); } - return new integer(getAttr("fullWidthPixels", () -> config.fullWidthPixels)); + return new integer(getAttr("fullWidthPixels", () -> config.fullWidthPixels, true)); } /** @@ -333,6 +338,7 @@ * * @return current value of MAX-HEIGHT-CHARS attribute */ + @LegacyAttribute(name = "MAX-HEIGHT-CHARS") @Override public decimal getMaxHeightChars() { @@ -1866,7 +1872,7 @@ { boolean prevDelayedMinimize = getAttr("delayedMinimize", () -> config.delayedMinimize); int prevState = getAttr("windowState", () -> config.windowState); - boolean realized = getAttr("realized", () -> config.realized); + boolean realized = getAttr("realized", () -> config.realized, true); // the delayed minimized behavior comes in effect only when the window has been // realized; also note that in this case the WINDOW-STATE attribute value is not affected! @@ -2027,7 +2033,7 @@ @Override public void setHidden(boolean hidden) { - if (config.hidden == hidden && (getAttr("visible", () -> config.visible) || config.hidden)) + if (config.hidden == hidden && (getAttr("visible", () -> config.visible, true) || config.hidden)) { // be a no-op only if the widget has been realized. return; @@ -2059,7 +2065,7 @@ { return new integer(getAttr("windowWidthPixels", () -> config.windowWidthPixels) != BaseConfig.INV_COORD ? getAttr("windowWidthPixels", () -> config.windowWidthPixels) : - getAttr("widthPixels", () -> config.widthPixels)); + getAttr("widthPixels", () -> config.widthPixels, true)); } /** @@ -2071,7 +2077,7 @@ { return new integer(getAttr("windowHeightPixels", () -> config.windowHeightPixels) != BaseConfig.INV_COORD ? getAttr("windowHeightPixels", () -> config.windowHeightPixels) : - getAttr("heightPixels", () -> config.heightPixels)); + getAttr("heightPixels", () -> config.heightPixels, true)); } /** === modified file 'src/com/goldencode/p2j/ui/chui/AlertBoxImpl.java' --- src/com/goldencode/p2j/ui/chui/AlertBoxImpl.java 2017-12-04 13:16:35 +0000 +++ src/com/goldencode/p2j/ui/chui/AlertBoxImpl.java 2020-10-24 22:32:57 +0000 @@ -2,7 +2,7 @@ ** Module : AlertBoxImpl.java ** Abstract : ** -** Copyright (c) 2011-2017, Golden Code Development Corporation. +** Copyright (c) 2011-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SIY 20111122 Extracted from AlertBox @@ -21,6 +21,7 @@ ** 011 VIG 20160315 Removed setUnderscoreOffset() call for button. ** 012 SBI 20161101 Added getTabItemList(). ** 013 HC 20171024 Runtime support for SYSTEM-DIALOG PRINT-SETUP and OUTPUT TO PRINTER. +** 014 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -456,9 +457,9 @@ { String s = (String) commons.content[i]; - Label ls = widgetFactory.createLabel(WidgetId.nextID(), - s, - -1); + ClientDrivenLabel ls = widgetFactory.createLabel(WidgetId.nextID(), + s, + -1); ls.config().dcolor = commons.alertColor; ls.setRightAligned(false); add(ls); === modified file 'src/com/goldencode/p2j/ui/chui/ChuiWidgetFactory.java' --- src/com/goldencode/p2j/ui/chui/ChuiWidgetFactory.java 2017-10-27 17:04:40 +0000 +++ src/com/goldencode/p2j/ui/chui/ChuiWidgetFactory.java 2020-10-24 22:32:57 +0000 @@ -2,7 +2,7 @@ ** Module : ChuiWidgetFactory.java ** Abstract : ** -** Copyright (c) 2011-2017, Golden Code Development Corporation. +** Copyright (c) 2011-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 SIY 20111221 Created initial version @@ -34,6 +34,7 @@ ** 022 OM 20170815 Added createFileSystemChooserDialog() method. ** 023 CA 20171020 Added support for FRAMEs sent to named or unnamed streams, in GUI. ** 024 OM 20171026 Updated implemenattion of createFileSystemChooserDialog(). +** 025 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* @@ -163,7 +164,7 @@ } /** - * Create new {@link Label} instance for given text. + * Create new {@link ClientDrivenLabel} instance for given text. * * @param id * The widget's ID. @@ -173,10 +174,10 @@ * The ID of the frame to which the label belongs, or -1 if unknown at the time of the * creation. * - * @return new {@link Label} instance. + * @return new {@link ClientDrivenLabel} instance. */ @Override - public LabelImpl createLabel(WidgetId id, String text, int frameId) + public ClientDrivenLabel createLabel(WidgetId id, String text, int frameId) { return new LabelImpl(id, text, frameId); } @@ -307,20 +308,20 @@ } /** - * Create UI-specific {@link Label} instance as a copy of given {@link Label}. + * Create UI-specific {@link ClientDrivenLabel} instance as a copy of given {@link ClientDrivenLabel}. * * @param id * The widget's ID. * @param labelInstance - * Source {@link Label} instance. + * Source {@link ClientDrivenLabel} instance. * @param frameId * The ID of the frame to which the label belongs, or -1 if unknown at the time of the * creation. * - * @return instance of UI-specific implementation of {@link Label}. + * @return instance of UI-specific implementation of {@link ClientDrivenLabel}. */ @Override - public LabelImpl createLabel(WidgetId id, Label labelInstance, int frameId) + public LabelImpl createLabel(WidgetId id, ClientDrivenLabel labelInstance, int frameId) { return new LabelImpl(id, labelInstance, frameId); } === modified file 'src/com/goldencode/p2j/ui/chui/ColumnLayout.java' --- src/com/goldencode/p2j/ui/chui/ColumnLayout.java 2017-04-01 23:33:34 +0000 +++ src/com/goldencode/p2j/ui/chui/ColumnLayout.java 2020-10-24 22:32:57 +0000 @@ -1,7 +1,7 @@ /* Module : ColumnLayout.java ** Abstract : Column LayoutManager implementation for CHUI ** -** Copyright (c) 2005-2017, Golden Code Development Corporation. +** Copyright (c) 2005-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 EVL 20050623 @21550 Creation initial version. @@ -46,6 +46,7 @@ ** 025 HC 20151013 Changes to extend GUI multi-window focus management and ACTIVE- ** WINDOW system handle processing to modal windows. ** 026 OM 20170329 Set the side widget for their labels. +** 027 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -307,8 +308,8 @@ text.length() - 2) + text; WidgetId nextId = c[i].getId().createId(WidgetId.nextID()); - Label label = (Label) container.screen().getFactory() - .createLabel(nextId, + ClientDrivenLabel label = container.screen().getFactory() + .createLabel(nextId, text + ": ", c[i].config().frameId); label.setLocation(curCol, curRow); === modified file 'src/com/goldencode/p2j/ui/chui/EditorImpl.java' --- src/com/goldencode/p2j/ui/chui/EditorImpl.java 2017-11-26 19:24:12 +0000 +++ src/com/goldencode/p2j/ui/chui/EditorImpl.java 2020-10-24 22:32:57 +0000 @@ -2,7 +2,7 @@ ** Module : EditorImpl.java ** Abstract : ChUI editor implementation ** -** Copyright (c) 2011-2017, Golden Code Development Corporation. +** Copyright (c) 2011-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SIY 20111121 Extracted CHUI-specific part from Editor. @@ -22,6 +22,7 @@ ** 011 EVL 20151104 Fix for '\r' line separators in incoming character data. All these characters ** are removed in ChUI. ** 012 CA 20171126 If read-only state changes, update the insert mode. +** 013 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -293,8 +294,9 @@ if (label != null) { - label.config().dcolor = config().dcolor; - label.config().pfcolor = config().pfcolor; + BaseConfig bc = label.config(); + bc.dcolor = config().dcolor; + bc.pfcolor = config().pfcolor; } } === modified file 'src/com/goldencode/p2j/ui/chui/LabelImpl.java' --- src/com/goldencode/p2j/ui/chui/LabelImpl.java 2017-04-01 23:33:34 +0000 +++ src/com/goldencode/p2j/ui/chui/LabelImpl.java 2020-10-24 22:32:57 +0000 @@ -2,7 +2,7 @@ ** Module : LabelImpl.java ** Abstract : ** -** Copyright (c) 2011-2017, Golden Code Development Corporation. +** Copyright (c) 2011-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description--------------------------------- ** 001 SIY 20111126 Extracted from Label @@ -19,6 +19,7 @@ ** 008 CA 20150222 Fixed label's COLUMN and WIDTH-CHARS attributes (in ChUI). ** 009 CA 20150323 Pass the frame ID when creating a label (to properly resolve the font in GUI). ** 010 CA 20150406 Added APIs to compute the label's width/height in native units. +** 011 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -85,7 +86,7 @@ */ @SuppressWarnings("unchecked") public class LabelImpl -extends Label +extends ClientDrivenLabel { /** * Constructs label with given text. @@ -114,7 +115,7 @@ * The ID of the frame to which the label belongs, or -1 if unknown at the time of the * creation. */ - LabelImpl(WidgetId id, Label label, int frameId) + LabelImpl(WidgetId id, ClientDrivenLabel label, int frameId) { super(id, label, frameId); } @@ -211,4 +212,4 @@ disableEmptyMode = false; } -} \ No newline at end of file +} === modified file 'src/com/goldencode/p2j/ui/chui/ThinClient.java' --- src/com/goldencode/p2j/ui/chui/ThinClient.java 2020-10-04 18:23:10 +0000 +++ src/com/goldencode/p2j/ui/chui/ThinClient.java 2021-01-28 17:31:08 +0000 @@ -2,7 +2,7 @@ ** Module : ThinClient.java ** Abstract : Thin Client class for generic UI driver implementation ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ---------------------------------Description------------------------------------ ** 001 EVL 20050710 @21707 Creation based on general concept. Necessary @@ -2622,6 +2622,30 @@ ** SBI 20200916 Changed continueEditing to apply screen buffers from the client side. ** CA 20200923 Changed StateSynchronizer to return Externalizable instances. ** HC 20200927 Batching of server config changes. +** CA 20201011 Optimized frame processing in getChanges(). +** HC 20201013 Server-pushed config field changes are serialized with field ids instead of +** names. +** CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation. +** GES 20201012 Pushed processing back into screen-buffer to improve encapsulation. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** GES 20201025 Reworked screen-buffer processing to be aware of the range of widget IDs so +** that the more efficient sparse array contiguous mode can be used. +** CA 20201102 In GUI, if focus is a BUTTON, then mnemonics work without ALT modifier. +** CA 20201112 Avoid pushing the entire frame definition when setting the FRAME or PARENT +** attribute. +** CA 20201117 Added API to delete a widget at runtime. +** SVL 20201126 Fixed on-click trigger targets within browse for the case when an in-browse +** editor is active. +** SVL 20201130 Browse column can be a valid focus target. +** CA 20201201 In case of a TEXT:READ-ONLY=true and TEXT:HIDDEN=true widget, the ENABLE statement +** will not make it visible. +** SVL 20201207 Added refreshBrowseColumnBuffers. +** CA 20210118 Performance improvements for get/setScreenBuffer - minimize widget lookups. +** CA 20210123 If an APPLY "ENTRY" is executed is sent even while within another trigger, the +** pendingFocus must be set. This check is done only if running under Windows OS, +** but the initial approach to disallow while in another trigger may be related to +** Linux ChUI (or the root cause is not correct). +** CA 20210128 Fixed problems with navigating a menubar via ALT and LEFT/RIGHT/UP/DOWN keys. */ /* @@ -2688,6 +2712,7 @@ import java.util.function.*; import java.util.logging.*; +import com.goldencode.util.Stack; import com.goldencode.p2j.cfg.*; import com.goldencode.p2j.email.*; import com.goldencode.p2j.comauto.*; @@ -4066,7 +4091,6 @@ setScreenBuffers(inbuf); Widget inFocus = locateInFocus(widgetId); - int inFocusState = FocusManager.focusState(inFocus); Frame tmpFrame = UiUtils.locateFrame(inFocus); if (tmpFrame != null) @@ -4074,11 +4098,7 @@ inFocus = tmpFrame.getField(inFocus); } - if (triggerNesting > 0 && - (inFocus instanceof FixedSizeContainer || inFocus instanceof Button) && - inFocus.isVisible() && - inFocus.isEnabled() && - (tmpFrame == null || tmpFrame.isVisible())) + if (triggerNesting > 0 && isValidFocusTarget(inFocus, tmpFrame)) { // no pending is used after trigger invocation if target is valid // before trigger invocation @@ -4132,11 +4152,7 @@ if (triggerNesting > 0 && !frameChangeEvent) { - boolean targetValid = (inFocus instanceof FixedSizeContainer || - inFocus instanceof Button) && - inFocus.isVisible() && - inFocus.isEnabled() && - (tmpFrame == null || tmpFrame.isVisible()); + boolean targetValid = isValidFocusTarget(inFocus, tmpFrame); if (!explicit) { @@ -4229,8 +4245,7 @@ currentEventList = save; return inbuf == null ? null - : getEditableClientScreenBuffers( - getScreenBuffer(inbuf[0].getBaseId(), inbuf[0].size(), true)); + : getEditableClientScreenBuffers(getScreenBuffer(inbuf[0].getBaseId(), true)); } /** @@ -4360,6 +4375,26 @@ } /** + * Determines if the given widget in focus is a valid focus target. + * + * @param inFocus + * The widget in focus. + * @param inFocusFrame + * Parent frame of the widget in focus. Can be null. + * + * @return true if the given widget in focus is a valid focus target. + */ + private boolean isValidFocusTarget(Widget inFocus, Frame inFocusFrame) + { + return (inFocus instanceof FixedSizeContainer || + inFocus instanceof Button || + inFocus instanceof BrowseColumn) && + inFocus.isVisible() && + inFocus.isEnabled() && + (inFocusFrame == null || inFocusFrame.isVisible()); + } + + /** * Adjust focus before processing event. * * @param explicit @@ -5028,6 +5063,22 @@ } /** + * Refresh server-side screen buffers of the columns of the given browse. + * + * @param browse + * Target browse. + */ + public void refreshBrowseColumnBuffers(Browse browse) + { + ScreenBuffer buffer = putBrowseColumnValues(browse, null); + if (buffer != null) + { + ScreenBuffer[] buffers = new ScreenBuffer[] {buffer}; + server.refreshBuffers(buffers); + } + } + + /** * Delete the line of text in which the targeted editor's cursor currently resides. Leave * the cursor at the leftmost position of the same line number (which will be the line * previously below this one if the cursor is not already on the last line) or at the @@ -5558,7 +5609,7 @@ frame.setCurrentStatement(null); } - return resolveBuffer ? getEditableClientScreenBuffers(getScreenBuffer(frameId, -1, true)) + return resolveBuffer ? getEditableClientScreenBuffers(getScreenBuffer(frameId, true)) : null; } @@ -9555,8 +9606,7 @@ // cleanup currentEventList = save; - return getEditableClientScreenBuffers( - getScreenBuffer(inbuf[0].getBaseId(), inbuf[0].size())); + return getEditableClientScreenBuffers(getScreenBuffer(inbuf[0].getBaseId(), false)); } finally { @@ -9761,12 +9811,12 @@ * The current (after config change) visible frame state. * @param id * The widget ID. - * @param fnames - * The list of config field names. + * @param fids + * The list of config field ids. * @param values * The list of new config field values. */ - public void pushWidgetAttrInBatch(boolean frameVisible, int id, String[] fnames, Object[] values) + public void pushWidgetAttrInBatch(boolean frameVisible, int id, Integer[] fids, Object[] values) { ClientConfigManager mgr = ConfigManager.getInstance(); @@ -9776,9 +9826,9 @@ WidgetId wid = widget.getId(); WidgetConfig wcfg = widget.config(); - for (int i = 0; i < fnames.length; i++) + for (int i = 0; i < fids.length; i++) { - mgr.setConfigField(wid, wcfg, fnames[i], values[i]); + mgr.setConfigField(wid, wcfg, fids[i], values[i]); } }); } @@ -9840,6 +9890,87 @@ } /** + * Attach a widget to a frame, at runtime. The widget can be a dynamic widget or a full static frame. + * + * @param fid + * The parent frame's ID. + * @param wid + * The widget ID. + * @param cfg + * The widget's configuration. + */ + @Override + public void attachRuntimeWidget(int fid, int wid, WidgetConfig cfg) + { + int tempTerm = forceInteractive(); + this.savedTermRedir = tempTerm; + + try + { + Frame frame = (Frame) getWidget(fid); + + Frame rootFrame = frame.getRootFrame(); + FrameConfig rootFrameCfg = rootFrame.config(); + boolean ignorePaint = inRowDisplayTrigger || (!rootFrame.isVisible() && !rootFrameCfg.visible); + + eventDrawingBracket(rootFrame, ignorePaint, true, () -> + { + registry().attachRuntimeWidget(frame, wid, cfg); + }); + } + finally + { + restoreRedirection(tempTerm); + } + } + + /** + * Delete and detach a widget from a frame, at runtime. The widget can be a dynamic widget or a full + * static frame. + * + * @param fid + * The parent frame's ID. + * @param wid + * The widget ID. + */ + @Override + public void deleteDynamicWidget(int fid, int wid) + { + Widget w = getWidget(wid); + if (w == null) + { + return; + } + Widget label = w instanceof LabeledWidget ? ((LabeledWidget) w).getLabelInstance() : null; + + int tempTerm = forceInteractive(); + this.savedTermRedir = tempTerm; + + try + { + Frame frame = (Frame) getWidget(fid); + + Frame rootFrame = frame.getRootFrame(); + FrameConfig rootFrameCfg = rootFrame.config(); + boolean ignorePaint = inRowDisplayTrigger || (!rootFrame.isVisible() && !rootFrameCfg.visible); + + eventDrawingBracket(rootFrame, ignorePaint, true, () -> + { + removeWidget(w); + + if (label != null) + { + removeWidget(label); + } + }); + } + finally + { + restoreRedirection(tempTerm); + } + } + + /** * Process array of frame/widget definitions provided by server. * * @param sd @@ -9932,28 +10063,6 @@ preprocessFrameDef(frame, cc); } - // remove deleted dynamic widgets - for (int id : definition.getDeletedWidgets()) - { - Widget w = getWidget(id); - Widget label = null; - - if (w instanceof LabeledWidget) - { - label = ((LabeledWidget) w).getLabelInstance(); - } - - if (w != null) - { - removeWidget(w); - } - - if (label != null) - { - removeWidget(label); - } - } - registry().pushDefinition(new ScreenDefinition[] { definition }); }); } @@ -11061,8 +11170,9 @@ int frameId = registry().lookupFrameIdFromWidgetId(widgetId); - ScreenBuffer[] sb = (frameId == -1 ? null : getEditableClientScreenBuffers( - getScreenBuffer(frameId, -1, true))); + ScreenBuffer[] sb = (frameId == -1) ? null + : getEditableClientScreenBuffers(getScreenBuffer(frameId, true)); + Frame frame; if (frameId != -1) { @@ -11750,7 +11860,7 @@ startEditingMode(focusWidgetId, el); // editing phrase needs to know about the frame and the focus... - result = getEditableClientScreenBuffers(getScreenBuffer(frameId, -1, true)); + result = getEditableClientScreenBuffers(getScreenBuffer(frameId, true)); result[0].putKeyCode(lastKey, lastKeyState); FrameData fd = frame.getFrameValue(null); result[0].setFrameValue(fd.getValue()); @@ -12212,12 +12322,18 @@ final Boolean[] allDisplayed = new Boolean[1]; allDisplayed[0] = false; + boolean isEnable = stmt == UIStatement.ENABLE; final Runnable doVisible = () -> { for (int i = 0; i < list.length; i++) { if (list[i] != null) { + if (isEnable && list[i].ignoreEnable()) + { + continue; + } + boolean viewed = false; UIStatement savedValue = null; if (list[i] instanceof Frame) @@ -12262,12 +12378,7 @@ { Set iteration = frame.getIterationIds(); - Iterator iter = frameBuf[0].getWidgetIDIterator(); - - while (iter.hasNext()) - { - iteration.remove(iter.next()); - } + frameBuf[0].removeWidgetIDs(iteration); allDisplayed[0] = (iteration.size() == 0); } @@ -12417,8 +12528,9 @@ updatedHeaders != null && !updatedHeaders.isEmpty()) { + WidgetRegistry registry = registry(); for (Integer hid : updatedHeaders) - registry().getComponent(hid.intValue()).repaint(); + registry.getComponent(hid.intValue()).repaint(); } needPause = true; @@ -12547,6 +12659,11 @@ { if (list[i] != null) { + if (isEnable && list[i].ignoreEnable()) + { + continue; + } + if (!list[i].isVisible()) { final int idx = i; @@ -13204,8 +13321,11 @@ boolean explicit = false; boolean pendingExplicit = false; + // TODO: CA: the underWindows check is added because at this time I can't confirm that the fix is valid + // under linux ChUI for 9.x 4GL versions. Testing under linux ChUI for 10.x shows that underWindows the + // 'not in nested trigger mode' check is not OK. This is left as a precaution for linux 9.x ChUI. if (focusWidgetId == -1 && - triggerNesting == 0 && + (triggerNesting == 0 || underWindows) && pendingFocus != null && pendingFocus.focusTraversable()) { @@ -13904,7 +14024,7 @@ if (inbuf != null) { result = getEditableClientScreenBuffers( - getScreenBuffer(inbuf[0].getBaseId(), inbuf[0].size(), force)); + getScreenBuffer(inbuf[0].getBaseId(), force)); } if (restoreFocus) @@ -14511,7 +14631,7 @@ (sbDefault == null || nextFrame.getId().asInt() != sbDefault.getBaseId())) { // create screen buffer to the given frame - ScreenBuffer moreScreenBuff = getScreenBuffer(nextFrame.getId().asInt(), -1, false); + ScreenBuffer moreScreenBuff = getScreenBuffer(nextFrame.getId().asInt(), false); // and add it to the list for further processing if (moreScreenBuff != null) { @@ -15115,17 +15235,15 @@ continue; } - int[] list = UiUtils.getFrameIDs((TopLevelWindow) wnd); - for (int i : list) + List list = ((TopLevelWindow) wnd).getFrames(); + for (Frame frame : list) { + int frameId = UiUtils.getWidgetIdAsInt(frame); // skip tinyInput frame - if (i == WidgetId.INPUT_FRAME_ID.asInt()) + if (frameId == WidgetId.INPUT_FRAME_ID.asInt()) continue; - Frame frame = (Frame) getWidget(new WidgetId(i)); - - if ((frame == null) || !frame.isDown() || frame.getDown() == 0 || - frame.isRedirected()) + if ((frame == null) || !frame.isDown() || frame.getDown() == 0 || frame.isRedirected()) continue; if (ids == null) @@ -15134,7 +15252,7 @@ down = new ArrayList<>(); } - ids.add(i); + ids.add(frameId); down.add(frame.getDown()); } } @@ -15818,6 +15936,49 @@ } /** + * Put the values of the column widgets of the given browse into the given screen buffer. + * + * @param browse + * Target browse. + * @param buffer + * Target buffer. If it is null, a new buffer will be created (if there are values to + * put). + * + * @return target buffer containing the widget values or null if there is no initial target + * buffer and there are no values to put. + */ + public ScreenBuffer putBrowseColumnValues(Browse browse, ScreenBuffer buffer) + { + int[] ids = browse.getIdsOfColumnWidgets(); + if (ids.length == 0) + { + return buffer; + } + + ScreenBuffer targetBuffer = buffer; + if (targetBuffer == null) + { + Frame frame = UiUtils.locateFrame(browse); + targetBuffer = new ScreenBuffer(frame.getId().asInt(), ids.length); + } + + for (int columnId : ids) + { + BrowseColumn col = (BrowseColumn) registry().getComponent(columnId); + + // This code is also for browse:VALIDATE() function which requires passing values of all + // editable columns to server side, in order to validate current row + // TODO make getState() work for browse column widgets + // at the moment column values are not cached on the server (with the exception of values + // for ROW-DISPLAY trigger) and so must be always sent to the server + targetBuffer.setState(col.getId().asInt(), ScreenBuffer.CHANGED); + putWidgetValue(col, targetBuffer); + } + + return targetBuffer; + } + + /** * Prepare message text for specified time to wait. * * @param millis @@ -16279,7 +16440,7 @@ // if the key is ALT, send it to the window if (key.keyCode() == Key.VK_ALT && !isChui()) { - focused.ancestor().processEvent(key); + eventDrawingBracket(focused.window(), () -> focused.ancestor().processEvent(key)); moreKeys = false; } else if (currentEventList.isGoEvent(Keyboard.eventName(key.keyCode())) || @@ -16378,42 +16539,22 @@ int frameId = frame.getId().asInt(); - return getScreenBuffer(frameId, -1, false); - } - - /** - * Generate a copy of the ScreenBuffer instance for the given - * frame. - * - * @param frameId - * The frame whose screen buffer must be returned. - * @param size - * The number of widgets in the screen buffer in total or -1 - * to allow this value to be calculated. - * - * @return ScreenBuffer instance. - */ - private ScreenBuffer getScreenBuffer(int frameId, int size) - { - return getScreenBuffer(frameId, size, false); - } - - /** - * Generate a copy of the ScreenBuffer instance for the given - * frame. - * - * @param frameId - * The frame whose screen buffer must be returned. - * @param size - * The number of widgets in the screen buffer in total or -1 - * to allow this value to be calculated. + return getScreenBuffer(frameId, false); + } + + /** + * Generate a copy of the ScreenBuffer instance for the given + * frame. + * + * @param frameId + * The frame whose screen buffer must be returned. * @param force * true to copy the current data into the screen * buffer without regard to the changed flag. * * @return ScreenBuffer instance. */ - private ScreenBuffer getScreenBuffer(int frameId, int size, boolean force) + private ScreenBuffer getScreenBuffer(int frameId, boolean force) { if (frameId == -1) { @@ -16425,45 +16566,34 @@ // another NPE protection if (frame == null) { - // this conidition is pretty rare, one known case is when the frame from editable frame + // this condition is pretty rare, one known case is when the frame from editable frame // list does not exist in general widget registry, can be a kind if race or timing LOG.log(Level.WARNING, "Unable to get the screen buffer for frame ID: "+frameId+ ". The frame with given ID is missing in widget registry."); return null; } + + WidgetRegistry.IdRange range = new WidgetRegistry.IdRange(); // get the list of all widgets - WidgetId[] list = registry().getIDs(frameId); + Widget[] list = registry().getWidgets(frame, range); int lSize = (list != null) ? list.length : 0; - // find out the maximum number of widgets, if needed - if (size == -1) - { - size = 0; - // do not process any widgets which are client-side only (negative IDs) - for (int i = 0; i < lSize; i++) - { - if (!WidgetId.virtualWidget(list[i])) - { - size ++; - } - } - } - // create the ScreenBuffer that big Widget comp = null; WidgetConfig config = null; - ScreenBuffer buffer = new ScreenBuffer(frameId, size); + ScreenBuffer buffer = new ScreenBuffer(frameId, range.min, range.max); for (int i = 0; i < lSize; i++) { - // skip labels or other widgets widhout server-side counter part - if (WidgetId.virtualWidget(list[i])) + comp = list[i]; + + // skip labels or other widgets without server-side counter part + if (WidgetId.virtualWidget(comp.getId())) continue; - int id = list[i].asInt(); - comp = registry().getComponent(list[i]); + int id = list[i].getId().asInt(); // WARNING: short-cut taken here! Since the only caller that uses // force flag today is CHOOSE, we use this flag for a @@ -16512,21 +16642,7 @@ } else if (comp instanceof Browse) { - Browse browse = (Browse) comp; - int[] ids = browse.getIdsOfColumnWidgets(); - - for (int columnId : ids) - { - BrowseColumn col = (BrowseColumn) registry().getComponent(columnId); - - // This code is also for browse:VALIDATE() function which requires passing values of all - // editable columns to server side, in order to validate current row - // TODO make getState() work for browse column widgets - // at the moment column values are not cached on the server (with the exception of values - // for ROW-DISPLAY trigger) and so must be always sent to the server - buffer.setState(col.getId().asInt(), ScreenBuffer.CHANGED); - putWidgetValue(col, buffer); - } + putBrowseColumnValues((Browse) comp, buffer); } processSideLabel(buffer, comp); @@ -16725,12 +16841,15 @@ // iterate through all widgets and push new values and state as needed, but only for the // current row... - WidgetId[] list = registry().getIDs(frameId); + WidgetRegistry registry = registry(); + Widget[] list = registry.getWidgets(frame, null); if (list != null) { - for (WidgetId wid : list) + for (Widget comp : list) { + WidgetId wid = comp.getId(); + if (frame.isDown()) { WidgetId resolved = Frame.resolveWidgetId(frame, wid.asInt()); @@ -16750,14 +16869,6 @@ continue; } - // get the widget - Widget comp = registry().getComponent(id); - - if (comp == null) - { - continue; - } - if (frame.isDown()) comp = frame.getField(comp); @@ -16864,29 +16975,24 @@ */ private ArrayList updateHeaders(ScreenBuffer frameBuf) { - Iterator iter = frameBuf.getHeaderIDIterator(); - BaseDataType value; - ArrayList updated = new ArrayList<>(); - - while (iter.hasNext()) + WidgetRegistry registry = registry(); + ArrayList updated = new ArrayList<>(); + + frameBuf.processHeaders((Integer id, Object value) -> { - Integer hid = iter.next(); - - Widget comp = registry().getComponent(hid); + Widget comp = registry.getComponent(id); if (comp instanceof DataContainer) { - value = (BaseDataType) frameBuf.getHeaderValue(hid); - // value is set regardless if changed or not - ((DataContainer) comp).setValue(value); + ((DataContainer) comp).setValue((BaseDataType) value); if (!value.equals(((DataContainer) comp).getValue())) { - updated.add(hid); + updated.add(id); } - } - } + } + }); return updated; } @@ -16973,7 +17079,7 @@ } return resolveBuffer && frame.isRealized() - ? getScreenBuffer(UiUtils.getWidgetIdAsInt(frame), -1, true) + ? getScreenBuffer(UiUtils.getWidgetIdAsInt(frame), true) : null; } @@ -17590,7 +17696,7 @@ setTriggerNesting(triggerNesting + 1); - server.help(lastKey, getEditableClientScreenBuffers(getScreenBuffer(focusedFrameId, -1))); + server.help(lastKey, getEditableClientScreenBuffers(getScreenBuffer(focusedFrameId, false))); setTriggerNesting(triggerNesting - 1); @@ -18615,7 +18721,6 @@ !(src instanceof FillIn || src instanceof Editor || src instanceof SelectionList || - src instanceof Button || src instanceof ComboBox || src instanceof Browse)) && src.enclosingFrame().isPresent()) @@ -18812,7 +18917,7 @@ Widget legacyMouseSource; - if (mouseSource instanceof Label) + if (mouseSource instanceof ClientDrivenLabel) { Frame temp = UiUtils.locateFrame(mouseSource); legacyMouseSource = temp != null ? temp : mouseSource.getLegacyWidget(); @@ -18880,8 +18985,51 @@ // from here on, we need to work with the browse if (legacyMouseSource != null && legacyMouseSource.parent(Browse.class) != null) { - legacyMouseSource = legacyMouseSource.parent(Browse.class); - } + if (!isChui() && evtID == MouseEvt.MOUSE_CLICKED) + { + // GUI browse click case. + if (legacyMouseSource instanceof FillIn || + legacyMouseSource instanceof ComboBox) + { + // BrowseColumn is the legacyMouseSource + legacyMouseSource = legacyMouseSource.getLegacyWidget(); + } + else if (legacyMouseSource instanceof ToggleBox) + { + // for some reason in 4GL triggers are not invoked on subsequent clicks on a toggle box + legacyMouseSource = null; + } + else if (legacyMouseSource instanceof BrowseColumn) + { + BrowseGuiImpl browse = (BrowseGuiImpl) legacyMouseSource.parent(Browse.class); + if (browse.getClickStartedEditColumn() == null) + { + legacyMouseSource = browse; + } + // else leave BrowseColumn as the legacyMouseSource + } + else + { + legacyMouseSource = legacyMouseSource.parent(Browse.class); + } + } + else + { + legacyMouseSource = legacyMouseSource.parent(Browse.class); + } + } + else if (legacyMouseSource instanceof BrowseGuiImpl && evtID == MouseEvt.MOUSE_CLICKED) + { + // If we click on the empty area to the right of the rightmost column which is an editing column, + // editing is started and the trigger for this rightmost column is invoked. + BrowseGuiImpl browse = (BrowseGuiImpl) legacyMouseSource; + BrowseColumnGuiImpl clickColumn = browse.getClickStartedEditColumn(); + if (clickColumn != null) + { + legacyMouseSource = clickColumn; + } + } + if (newFocus != null && newFocus.parent(Browse.class) != null) { newFocus = newFocus.parent(Browse.class); @@ -19589,7 +19737,7 @@ { Browse brws = (Browse) src.parent(Browse.class); - if (inFocus.parent(Browse.class) != src.parent(Browse.class)) + if (inFocus != null && inFocus.parent(Browse.class) != src.parent(Browse.class)) { if (brws.isEditPossible()) { @@ -19721,7 +19869,10 @@ // allow the focus change events and rebalance the queue if (fire) { - if (triggerNesting == 0) + // TODO: CA: the underWindows check is added because at this time I can't confirm that the fix is valid + // under linux ChUI for 9.x 4GL versions. Testing under linux ChUI for 10.x shows that underWindows the + // 'not in nested trigger mode' check is not OK. This is left as a precaution for linux 9.x ChUI. + if (triggerNesting == 0 || underWindows) { // save target widget, so it can be used in subsequent root // level wait-for loops @@ -19982,7 +20133,7 @@ { Widget frame = UiUtils.locateFrame(widget); int frameId = UiUtils.getWidgetIdAsInt(frame); - return getEditableClientScreenBuffers(getScreenBuffer(frameId, -1)); + return getEditableClientScreenBuffers(getScreenBuffer(frameId, false)); } /** @@ -20053,7 +20204,7 @@ -1, -1, -1, - getScreenBuffer(frameId, -1), + getScreenBuffer(frameId, false), save); } @@ -20444,7 +20595,7 @@ UiUtils.lookupWidgetIdAsInt(other), resourceId, result.matchedWidgetId, - getScreenBuffer(focusFrameId, -1, true), + getScreenBuffer(focusFrameId, true), save); } @@ -21806,7 +21957,7 @@ { for (Widget widget : widgets) { - if (widget.isVisible() && UiUtils.isAllShowing(widget, frames)) + if (widget != null && widget.isVisible() && UiUtils.isAllShowing(widget, frames)) { widget.repaint(); } @@ -24726,7 +24877,7 @@ int i = 0; while (i < widgetAttrs.length) { - ArrayList fnames = new ArrayList<>(); + ArrayList fids = new ArrayList<>(); ArrayList values = new ArrayList<>(); int widgetId = -1; @@ -24744,7 +24895,7 @@ break; } - fnames.add((String) widgetAttrs[i + 1]); + fids.add((Integer) widgetAttrs[i + 1]); values.add(widgetAttrs[i + 2]); i += 3; @@ -24752,10 +24903,10 @@ pushWidgetAttrInBatch(false, widgetId, - fnames.toArray(new String[0]), + fids.toArray(new Integer[0]), values.toArray()); - fnames.clear(); + fids.clear(); values.clear(); } } === modified file 'src/com/goldencode/p2j/ui/client/Browse.java' (properties changed: -x to +x) --- src/com/goldencode/p2j/ui/client/Browse.java 2020-09-16 00:56:38 +0000 +++ src/com/goldencode/p2j/ui/client/Browse.java 2020-12-08 14:00:14 +0000 @@ -361,6 +361,11 @@ ** process is running. ** AIL 20200827 An error is thrown when trying to set column value when no row is selected. ** SVL 20200913 isRowSelected is fixed for empty rows. +** SVL 20201116 Use setBrowseColumn instead of setBrowse. +** CA 20201201 FillIn.update(boolean) must validate the format only when the event loop is +** finished or focus is lost. Until then, the triggers must see the actual +** SCREEN-VALUE that the user entered, even if the value is not validated. +** SVL 20201207 On row change refresh column values on the server. */ /* @@ -3346,7 +3351,7 @@ if (!undo && isEditing()) { - if (isEditorFillIn() && !getActiveFillIn().update()) + if (isEditorFillIn() && !getActiveFillIn().update(true)) { return false; } @@ -3842,8 +3847,8 @@ screen().getRegistry().addWidget(editorId, fillIn); fillIn.initialize(editorId, cfg); - // reset underline attribute and force Browse colors - fillIn.setBrowse(this); + BrowseColumn column = (BrowseColumn) ThinClient.getInstance().getWidget(cols[currColumn].id); + fillIn.setBrowseColumn(column); // fixup cell alignment if (BrowseTextAlignment.RIGHT.equals(getTextAlign(cols[currColumn]))) @@ -4896,12 +4901,15 @@ this.currentRow = currRow; currentRowAvail = false; } - + if (updateSelection) { currRowSelected = true; } } + + // Refresh column SCREEN-VALUEs on the server. + ThinClient.getInstance().refreshBrowseColumnBuffers(this); } /** === added file 'src/com/goldencode/p2j/ui/client/ClientDrivenLabel.java' --- src/com/goldencode/p2j/ui/client/ClientDrivenLabel.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/ui/client/ClientDrivenLabel.java 2021-01-25 13:46:58 +0000 @@ -0,0 +1,758 @@ +/* +** Module : ClientDrivenLabel.java +** Abstract : Abstract base class for client-driven labels. +** +** Copyright (c) 2005-2021, Golden Code Development Corporation. +** +** -#- -I- --Date-- ----------------------------------------Description--------------------------------------- +** 001 HC 20201024 Created initial version. +** CA 20210125 The 4054/5905 error messages show when the new label must be aware of the NO-ERROR mode. +** Also, the label is set even if it doesn't fit. +*/ +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + +package com.goldencode.p2j.ui.client; + +import java.util.Objects; + +import com.goldencode.p2j.ui.*; +import com.goldencode.p2j.ui.chui.ThinClient; +import com.goldencode.p2j.ui.client.widget.*; +import com.goldencode.p2j.util.ErrorManager; + +/** + * Abstract base class for client-driven labels. + */ +public abstract class ClientDrivenLabel> +extends FixedSizeContainer +implements WidgetWithConfig, + Label +{ + /** Widget's size which is set by setSize method. */ + private boolean forcedSize = false; + + /** Component attribute container. */ + protected LabelConfig config = null; + + /** Make label aligned to right boundary. */ + protected boolean rightAligned = true; + + /** Display additional line */ + protected boolean underlined = false; + + /** Flag the special case of use as a separator line. */ + private boolean delimiter = false; + + /** Label right shift offset. */ + protected double offset = 0; + + /** If it is true, empty string is displayed.*/ + protected boolean emptyMode = false; + + /** Prepare for disabling empty mode. */ + protected boolean disableEmptyMode = false; + + /** The side-widget associated with this label, if this is a side-label. */ + protected Widget sideWidget = null; + + /** + * Constructs label with given text. + * + * @param id + * The widget's ID. + * @param text + * Initial text. + * @param frameId + * The ID of the frame to which the label belongs, or -1 if unknown at the time of the + * creation. + */ + public ClientDrivenLabel(WidgetId id, String text, int frameId) + { + config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); + config.frameId = frameId; + + setName(text); + } + + /** + * Create a copy of the label. + * + * @param id + * The widget's ID. + * @param label + * Label to copy parameters from. + * @param frameId + * The ID of the frame to which the label belongs, or -1 if unknown at the time of the + * creation. + */ + public ClientDrivenLabel(WidgetId id, ClientDrivenLabel label, int frameId) + { + config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); + config.frameId = frameId; + + setName(label.config.name); + + setUnderlined(label.isUnderlined()); + setRightAligned(label.isRightAligned()); + setWidth(label.width()); + forcedSize = label.forcedSize; + setLocation(label.location()); + setVisible(label.isVisible()); + } + + /** + * Retrieve the widget's width in native units. + * + * @return See above. + */ + protected abstract int nativeWidth(); + + /** + * Retrieve the widget's height in native units. + * + * @return See above. + */ + protected abstract int nativeHeight(); + + /** + * Set the specified widget as this side-label's {@link #sideWidget}. + * + * @param widget + * The side-widget for this label. + */ + public void setSideWidget(Widget widget) + { + this.sideWidget = widget; + if (widget instanceof LabeledDataContainer) + { + Label prevLabel = ((LabeledDataContainer) widget).getLabelInstance(); + if (prevLabel != this && prevLabel != null) + { + this.config.applyConfig(prevLabel.config()); + } + } + updateSize(); + } + + /** + * Get the side-widget associated with this label. + * + * @return The {@link #sideWidget}. + */ + public Widget getSideWidget() + { + return sideWidget; + } + + /** + * Replacement API for the logic which should exist in the widget's c'tor, but the logic + * can't be executed as it depends on the widget being registered into the official registry. + *

+ * This API is called after the widget is created and added to the registry. + * + * @param id + * The widget's ID. + * @param cfg + * The config used to initialize this widget. + */ + @Override + public void initialize(WidgetId id, LabelConfig cfg) + { + config = ConfigManager.getInstance().resolveWidgetConfig(id, cfg); + ConfigManager.getInstance().replaceConfig(config, cfg); + } + + /** + * Create a copy of the label. + * + * @param id + * The widget's ID. + * @param label + * Label to copy parameters from. + */ + public ClientDrivenLabel(WidgetId id, ClientDrivenLabel label) + { + config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); + + config.name = label.config.name; + setUnderlined(label.isUnderlined()); + setRightAligned(label.isRightAligned()); + setWidth(label.width()); + forcedSize = label.forcedSize; + setLocation(label.location()); + setVisible(label.isVisible()); + } + + /** + * Provide access to the attributes stored in the widget. + * + * @return Reference to attribute container. + */ + @SuppressWarnings("unchecked") + @Override + public LabelConfig config() + { + return config; + } + + /** + * Marks this label as one used to draw a separator line for header + * widgets. In this case there is no label text but the width is + * positive and the underline attribute is enabled. + * + * @param delimiter + * true to set this as a separator line. + */ + public void setDelimiter(boolean delimiter) + { + this.delimiter = delimiter; + } + + /** + * Gets the flag that marks this label as one used to draw a separator + * line for header widgets. + * + * @return true if this label is a separator line. + */ + public boolean isDelimiter() + { + return delimiter; + } + + /** + * Draws text. Screen value is left justified or padded to the right bound + * with spaces. Non first lines are filled with spaces. + */ + @Override + public abstract void draw(); + + /** + * Sets position of terminal's cursor. Really does nothing because this + * widget will never get an input focus. + */ + @Override + public void drawCaret() + { + } + + /** + * Sets new widget's size. After call to this method the widget will + * not change size automatically accordingly to screen value changes. + * + * @param width + * New widget width. + * @param height + * New widget height. + */ + @Override + public void setSize(double width, double height) + { + forcedSize = true; + super.setSize(width, height); + } + + /** + * Set widget width. + * + * @param width + * New widget width. + */ + @Override + public void setWidth(double width) + { + forcedSize = true; + super.setWidth(width); + } + + /** + * Set widget height. + * + * @param height + * New widget height. + */ + @Override + public void setHeight(double height) + { + forcedSize = true; + super.setHeight(height); + } + + /** + * This component will not receive an input focus when Tab or Shift-Tab is + * pressed. + * + * @return Always false. + */ + @Override + public boolean focusTraversable() + { + return false; + } + + /** + * Checks whether this attribute container can receive focus generally. + * + * @return true if this container can be focused under + * certain conditions. false if, and only if, this + * attribute container can never receive focus. + */ + @Override + public boolean isFocusable() + { + return false; + } + + /** + * Synchronizes the actual size with the default size. + */ + protected void updateSize() + { + if (forcedSize) + return; + + int nativeWith = nativeWidth(); + int nativeHeight = nativeHeight(); + + CoordinatesConversion cc = screen().coordinates(); + double width = cc.widthFromNative(nativeWith); + double height = cc.heightFromNative(nativeHeight); + + // call super to avoid setting forcedSize to true + super.setSize(width, height); + + // synchronize the new size with server + BaseConfig c = config(); + c.widthChars = width; + c.heightChars = height; + } + + /** + * Set underlining mode. + * + * @param b + * New value for the mode. + */ + public void setUnderlined(boolean b) + { + underlined = b; + updateSize(); + } + + /** + * Get current value of the underlining flag. + * + * @return true if label is underlined. + */ + public boolean isUnderlined() + { + return underlined; + } + + /** + * Get current value of the alignment. + * + * @return true if right alignment is active. + */ + public boolean isRightAligned() + { + return rightAligned; + } + + /** + * Activate or deactivate right alignment of the text. + * + * @param rightAligned + * true activate right alignment, + * false- deactivate. + */ + public void setRightAligned(boolean rightAligned) + { + this.rightAligned = rightAligned; + } + + /** + * Sets the name of the label. + * + * @param name + * New name of the component. + */ + public void setName(String name) + { + config.name = name; + + //Reset the forcedSize flag so updateSize() can properly adjust the size. + forcedSize = false; + updateSize(); + } + + /** + * Get widget name. + * + * @return widget name or empty string if is not set. + */ + @Override + public String name() + { + String name = super.name(); + return name == null ? "" : name; + } + + /** + * Set extra offset for label. + * + * @param offset + * Size of the shift. + */ + public void setOffset(double offset) + { + this.offset = Coordinate.scale(offset); + } + + /** + * Disable empty label mode. Note that actual disabling happens only during + * next {@link #draw()} call. + */ + public void setEmptyModeOff() + { + disableEmptyMode = true; + } + + /** + * Enable empty mode. + */ + public void setEmptyModeOn() + { + emptyMode = true; + } + + /** + * Process this label so that it's linked with the server-side widget represented by the + * serverSideId. + * + * @param frameId + * The ID of the frame to which this label belongs. + * @param serverSideId + * The server-side ID of this label. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void linkTo(int frameId, int serverSideId) + { + if (serverSideId == -1) + { + return; + } + + BaseConfig lblCfg = config(); + WidgetId oldId = lblCfg.id; + WidgetId newId = oldId.createId(serverSideId); + + ConfigManager cm = ConfigManager.getInstance(); + BaseConfig bc = cm.getActiveConfig(newId); + + if (bc != null) + { + Point loc = location(); + + double x = loc.x; + double y = loc.y; + boolean setLoc = false; + + double assignedCol = ConfigHelper.getAssignedColumn(bc); + double assignedRow = ConfigHelper.getAssignedRow(bc); + + if (assignedCol != BaseConfig.INV_COORD) + { + x = assignedCol; + setLoc = true; + } + + if (assignedRow != BaseConfig.INV_COORD) + { + y = assignedRow; + setLoc = true; + } + + if (setLoc) + { + setLocation(x, y); + } + + // inherit width, but only when it is not already set on this + if (bc.widthChars != -1 && width() == -1) + { + setWidth(bc.widthChars); + lblCfg.widthChars = bc.widthChars; + } + + lblCfg.align = bc.align; + + // restore the colors + copyColors(bc, lblCfg); + lblCfg.dcolor = bc.dcolor; + lblCfg.pfcolor = bc.pfcolor; + lblCfg.sysbgcolor = bc.sysbgcolor; + lblCfg.sysfgcolor = bc.sysfgcolor; + + // restore the font + lblCfg.font = bc.font; + } + + ClientConfigManager mgr = ConfigManager.getInstance(); + + // switch the internal ID for this label (from virtual to real) + mgr.switchId(oldId, newId); + + // mark as dirty all configs which are not the same as what the server has... + mgr.trackAllChanges(lblCfg); + + OutputManager.instance().getRegistry().addWidget((Widget) this); + } + + /** + * Update the {@code visible} flag in the configuration and in the widget's state. + *

+ * If the widget has just been realized and its width has not yet been set, it will be computed + * now. + * + * @param visible + * The widget's visible state. + */ + @Override + public void _setVisible(boolean visible) + { + boolean oldRealized = config.realized; + + super._setVisible(visible); + + if (!oldRealized && config.realized && config.widthChars == -1) + { + config.widthChars = width(); + } + } + + /** + * Get the state of the {@link #forcedSize} flag. + * + * @return See above. + */ + public boolean isForcedSize() + { + return forcedSize; + } + + /** + * Set the {@link #forcedSize} flag to the specified state. + * + * @param forcedSize + * The new state for the {@link #forcedSize} flag. + */ + public void setForcedSize(boolean forcedSize) + { + this.forcedSize = forcedSize; + } + + /** + * The method is called after the configuration associated with + * the implementor changes. The parameter points to a reference + * representing the original configuration state. This reference + * can be used to detect what configuration fields changed and so + * optimize any processing related to the configuration change. + * + * @param beforeUpdate + * Config reference capturing the config state before + * the owner's config was modified. + */ + @Override + public void afterConfigUpdate(LabelConfig beforeUpdate) + { + super.afterConfigUpdateBase(beforeUpdate); + + LabelConfig c = config(); + + setWidth(c.widthChars); + + // name changed? + if (!Objects.equals(((BaseConfig) beforeUpdate).name, c.name)) + { + //reset the forcedSize flag so updateSize() can properly adjust the size. + forcedSize = false; + updateSize(); + } + } + + /** + * Additional processing required when widget configuration is updated AFTER the frame layout + * has been performed. + */ + @Override + public void postprocessConfig() + { + super.postprocessConfig(); + + if (sideWidget != null) + { + double newLblWidth = 0; + double oldLblWidth = 0; + boolean labelChanged = false; + if (sideWidget instanceof LabeledDataContainer) + { + String newLabel = ((LabeledDataContainer) sideWidget).getLabelText() + ": "; + labelChanged = !newLabel.equals(config.name); + + if (labelChanged) + { + newLblWidth = ZeroColumnLayout.labelWidth((LabeledWidget) sideWidget, newLabel); + oldLblWidth = ZeroColumnLayout.labelWidth((LabeledWidget) sideWidget, + config.name); + } + } + + // check the left border + if (labelChanged) + { + double newColumn = Coordinate.scale(ConfigHelper.getEffectiveColumn(config) + + oldLblWidth - newLblWidth); + + if (newColumn < 0) + { + if (!ErrorManager.isSilent()) + { + Frame fr = (Frame) UiUtils.locateFrame(this); + + StringBuilder err = new StringBuilder(); + + if (screen().isChui()) + { + err.append("**Unable to set SCREEN-VALUE. ") + .append("LITERAL widget does not fit in FRAME ") + .append(fr.shortName()).append(". (4054)"); + } + else + { + err.append("**All or part of LITERAL widget is being placed outside of FRAME ") + .append(fr.shortName()) + .append(" by setting SCREEN-VALUE. (5905)"); + } + + ThinClient.getInstance().displayWarningMessage(err.toString()); + } + + // no need to restore the label string + } + else + { + Point loc = location(); + config.column = newColumn; + setLocation(newColumn, loc.y); + } + + // the label is set even if it doesn't fit + if (sideWidget.config() instanceof ControlConfig) + { + // setName will update the size, too + setName(((ControlConfig) sideWidget.config()).label + ": "); + } + } + } + } + + /** + * The method returns the legacy widget representing this widget. + * + * @return A non-wirtual widget reference, or null if none found. + */ + @Override + public Widget getLegacyWidget() + { + return getSideWidget(); + } + + /** + * Additional processing required when widget configuration is updated AFTER the frame layout + * has been performed. + * + * @param widget + * The widget to which this side-labe is attached. + */ + public void postprocessConfig(LabeledWidget widget) + { + // check the frame first - this call is only for non-side-labels + Frame frame = (Frame) UiUtils.locateFrame((Widget) widget); + if (frame.isSideLabels()) + { + return; + } + + String label = ZeroColumnLayout.getLabel(widget, frame.isSideLabels(), frame.hasHeaders()); + if (label != null && !label.equals(this.name())) + { + // TODO: if new width is greater than frame width, then the width remains as the + // current label's width - and the new label is trimmed to this width + setName(label); + } + } + + /** + * Check the direct manipulation capability of the widget. LABEL widget does not allow direct + * manipulation. + * + * @return TRUE if the widget is able to be directly manipulated, + * FALSE otherwise. + */ + @Override + public boolean isDirectManipulable() + { + return false; + } +} \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/client/ComboBox.java' --- src/com/goldencode/p2j/ui/client/ComboBox.java 2019-05-03 20:42:53 +0000 +++ src/com/goldencode/p2j/ui/client/ComboBox.java 2021-01-28 20:16:06 +0000 @@ -2,7 +2,7 @@ ** Module : ComboBox.java ** Abstract : combo-box implementation ** -** Copyright (c) 2005-2019, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------- ** 001 EVL 20050211 @19831 Creation based on CHARVA JComboBox class. @@ -228,6 +228,15 @@ ** 123 SBI 20190322 Changed refreshItems, formatValues and setValue to reuse format ** parameters between items in order to improve performance. ** 124 SVL 20190415 Support for browsed combo-boxes. +** 125 CA 20201011 EventDefinition uses a fast access map instead of an integer set, for widget IDs. +** SVL 20201116 Store reference to the parent browse column in order to invoke the column +** triggers. +** CA 20210111 Fixed the drop-down's selected value when is driven via the SCREEN-VALUE from +** server-side. +** Added ENTER as an event label for the drop-down, when running under ChUI Windows. +** CA 20210123 A better fix for the selected value when is driven via SCREEN-VALUE from +** server-side. +** EVL 20210128 Fix for NPE issue/regression. */ /* @@ -287,6 +296,8 @@ import java.util.*; +import org.roaringbitmap.*; + import com.goldencode.p2j.ui.*; import com.goldencode.p2j.ui.ComboBoxConfig.Mode; import com.goldencode.p2j.ui.chui.ThinClient; @@ -342,8 +353,8 @@ /** A helper field for drop-down related event handling. */ protected boolean endConditionRaised; - /** Parent browse widget, if any. */ - protected Browse browse = null; + /** Parent browse column widget (if this combo-box is associated to a browse). */ + protected BrowseColumn browseColumn = null; /** Component label. */ private Label label = null; @@ -581,6 +592,7 @@ { int key = keyEvent.actionCode(); + Browse browse = getBrowse(); if (browse != null && (key == Keyboard.KA_TAB || key == Keyboard.KA_BACK_TAB || key == Keyboard.KA_RETURN)) { @@ -670,20 +682,36 @@ ComboBoxModel model = model(); FormatParametersHolder params = new FormatParametersHolder(); + boolean forceFirst = ThinClient.getInstance().isChui() && + (config.pairs == null || !config.pairs) && + (value.isUnknown() || value.toStringMessage().trim().isEmpty()); + int selectedIndex = -1; - for (int i = 0; i < items.length; i++) + for (int i = 0; !forceFirst && i < items.length; i++) { if (items[i].getValue().toStringMessage().compareToIgnoreCase(value.toStringMessage()) == 0) { - initValue = items[i].getValue(); - - String text = items[i].getLabel().toStringMessage(); - model.select(formatValue(text, params.format, params.fmtWidth, params.right)); - model.setSelectedIndex(i); - + selectedIndex = i; break; } } + + if (items.length > 0 && forceFirst) + { + // select first item + selectedIndex = 0; + } + + if (selectedIndex != -1) + { + initValue = items[selectedIndex].getValue(); + + String text = items[selectedIndex].getLabel().toStringMessage(); + model.select(formatValue(text, params.format, params.fmtWidth, params.right)); + model.setSelectedValue(initValue); + model.setSelectedId(items[selectedIndex].getItemId()); + model.setSelectedIndex(selectedIndex); + } } /** @@ -855,7 +883,10 @@ items = config.items; } - maxlen = 0; + if (!config.realized) + { + maxlen = 0; + } String cbDataType = config.dataType; @@ -881,7 +912,7 @@ String text = formatValue(item.getLabel().toStringMessage(), format, width, right); - if (text.length() > maxlen) + if (!config.realized && text.length() > maxlen) { maxlen = text.length(); width = (maxlen > fmtWidth) ? maxlen : fmtWidth; @@ -899,7 +930,7 @@ config.selectedIndex = idx; if (idx > 0) { - character label = items[idx].getLabel(); + character label = config.items[idx].getLabel(); model.select(label == null ? null : label.toStringMessage().trim()); } @@ -1190,25 +1221,55 @@ } /** - * Link editor with the Browse widget. - * - * @param browse - * The instance of the Browse to link with. - */ - public void setBrowse(Browse browse) - { - this.browse = browse; - } - - /** - * Get the parent {@link Browse} widget, if any. - * - * @return the parent {@link Browse} widget if this combo-box is browsed or null - * otherwise. + * Link this combo-box with the BrowseColumn widget. + * + * @param browseColumn + * Parent browse column widget. + */ + public void setBrowseColumn(BrowseColumn browseColumn) + { + this.browseColumn = browseColumn; + } + + /** + * Determines whether this {@link ComboBox} has an associated {@link Browse} + * widget. + * + * @return true if this {@link ComboBox} has an associated + * {@link Browse} widget. + */ + public boolean isLinkedToBrowse() + { + return browseColumn != null; + } + + /** + * This method returns the widget that should serve as the source of the triggered event. This widget must + * always be a non-virtual widget. The method may also return {@code null} which prevents triggering of the + * legacy event.

+ * If this combo-box is inside a browse column, that browse column widget is returned. + * + * @param evtCode + * The event being triggered. + * @param isKey + * Whether the event being triggered is a real key. + * + * @return see above. + */ + public Widget getTriggerWidget(int evtCode, boolean isKey) + { + return isLinkedToBrowse() ? browseColumn.getTriggerWidget(evtCode, isKey) : + super.getTriggerWidget(evtCode, isKey); + } + + /** + * Get associated browse widget, if any. + * + * @return associated browse widget or null if there is no associated browse widget. */ public Browse getBrowse() { - return browse; + return browseColumn != null ? browseColumn.getBrowse() : null; } /** @@ -1263,7 +1324,7 @@ // add the exit event EventList list = new EventList(); Set events = initEventsSet(); - Set widgets = new HashSet<>(); + RoaringBitmap widgets = new RoaringBitmap(); widgets.add(fid); EventDefinition event = new EventDefinition(events, widgets, null, false); event.setTriggerId(-1); @@ -1315,8 +1376,15 @@ // will be evaluated just below if (focus == null) { - parent().setFocus(this); - focus = (Widget) UiUtils.getCurrentFocus(); + // class Container is used in roaring bitmap too so we have to define + // our one explicitely + com.goldencode.p2j.ui.client.widget.Container parent = parent(); + // MPE protection is required here + if (parent != null) + { + parent.setFocus(this); + focus = (Widget) UiUtils.getCurrentFocus(); + } } if (focus == this) @@ -1379,6 +1447,11 @@ { Set setResult = new HashSet<>(); setResult.add("RETURN"); + // if the ChUI client is on Windows, then add ENTER, too + if (ThinClient.getInstance().isUnderWindows()) + { + setResult.add("ENTER"); + } return setResult; } @@ -1452,8 +1525,7 @@ ControlSetItem item = items[ndx]; - return (item.getLabel() == null) || "".equals(item.getLabel().toStringMessage()) ? - new character() : item.getValue(); + return item.getValue(); } /** === modified file 'src/com/goldencode/p2j/ui/client/FillIn.java' --- src/com/goldencode/p2j/ui/client/FillIn.java 2020-10-08 21:34:55 +0000 +++ src/com/goldencode/p2j/ui/client/FillIn.java 2021-01-23 18:01:03 +0000 @@ -2,7 +2,7 @@ ** Module : FillIn.java ** Abstract : text field editor/display implementation ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SVG 20050609 @21446 Initial version. @@ -445,7 +445,15 @@ ** 208 HC 20200313 Javadoc fixes. ** 209 RFB 20200919 Added some protection in toString() and getScreenValue() to allow for debug ** to occur prior to some initialization (Ref #4861). -** 210 IAS 20201007 Re-wojed setValue using Type enum +** 210 IAS 20201007 Re-worked setValue using Type enum +** CA 20201102 update() must call finishEdit() only if we are not in editing mode. +** SVL 20201116 Store reference to the parent browse column in order to invoke the column +** triggers. +** CA 20201201 FillIn.update(boolean) must validate the format only when the event loop is +** finished or focus is lost. Until then, the triggers must see the actual +** SCREEN-VALUE that the user entered, even if the value is not validated. +** CA 20201210 If the frame is NO-AUTO-VALIDATE, the format will not be checked on LEAVE. +** CA 20210123 If the FILL-IN:DATA-TYPE is changed, clear the cached displayVar and such. */ /* @@ -503,6 +511,8 @@ package com.goldencode.p2j.ui.client; +import java.util.*; + import com.goldencode.p2j.ui.*; import com.goldencode.p2j.ui.chui.ThinClient; import com.goldencode.p2j.ui.client.event.*; @@ -552,9 +562,9 @@ /** Track if format set explicitly or it is determined automatically. */ private boolean autoFormat = false; - - /** Browse widget to notify. */ - protected Browse browse = null; + + /** Parent browse column widget (if this fill-in is associated to a browse). */ + protected BrowseColumn browseColumn = null; /** Flag to force left alignment. */ private boolean forceLeft = false; @@ -778,7 +788,7 @@ @Override public BaseDataType getValue() { - update(); + update(false); return getVariable(); } @@ -837,7 +847,7 @@ config.appDataPres.setInsertMode(Window.getInsertMode() == Window.INSERT_MODE_INS); } - if (browse != null) + if (isLinkedToBrowse()) { Widget oldFocus = (Widget) UiUtils.getCurrentFocus(); pendingNoZap = (oldFocus != null && @@ -922,7 +932,7 @@ inAutoReturn = false; } - if (browse == null) + if (!isLinkedToBrowse()) ins = config.appDataPres.getInsertMode(); } else @@ -936,7 +946,7 @@ skipCursorActivation = false; } - if (browse == null) + if (!isLinkedToBrowse()) ins = false; } @@ -1011,11 +1021,14 @@ /** * Updates variable's value with current field's content. * + * @param validate + * Flag indicating if the format should be validated. + * * @return Is update successful. true value may also mean * that FillIn contains unsupported (?????) value. It not * an error but variable's values isn't changed. */ - public boolean update() + public boolean update(boolean validate) { // null presentation implies that data has been not edited if (config.appDataPres == null) @@ -1023,13 +1036,13 @@ return true; } - if (config.appDataPres.finishEdit()) + if (validate && !ThinClient.getInstance().isInEditingBlock()) { - // add repainting if required - } + if (config.appDataPres.finishEdit()) + { + // add repainting if required + } - if (!ThinClient.getInstance().isInEditingBlock()) - { // do not check format if we are in an editing block! try { @@ -1180,7 +1193,7 @@ Color dc = UiUtils.getDColor(this); Color pf = config.pfcolor; - if (browse != null) + if (isLinkedToBrowse()) pf = config.dcolor; @SuppressWarnings("unchecked") @@ -1190,7 +1203,7 @@ if (!screen().isRedirected() && (!isVisible() || !frame.isVisible())) return; - Color color = ((config.widgetFocused || browse != null) && + Color color = ((config.widgetFocused || isLinkedToBrowse()) && isEnabled() && !inChoose) ? pf : dc; if (focusTraversable() && useUnderline && !inChoose) @@ -1489,7 +1502,7 @@ { if (isLinkedToBrowse()) { - EventManager.postEvent(new KeyInput(browse, + EventManager.postEvent(new KeyInput(getBrowse(), EventType.KEY_TYPED, Keyboard.KA_RETURN)); } @@ -1604,9 +1617,9 @@ // RETURN action key if (key == Keyboard.KA_RETURN) { - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -1678,9 +1691,9 @@ if (!completeEdit()) return; - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -1697,9 +1710,9 @@ if (key == Keyboard.KA_CURSOR_DOWN) { - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -1722,9 +1735,9 @@ if (!completeEdit()) return; - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } } @@ -1735,9 +1748,9 @@ if (!completeEdit()) return; - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -1755,9 +1768,9 @@ // CURSOR-UP action key if (key == Keyboard.KA_CURSOR_UP) { - if (browse != null) + if (isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -1872,7 +1885,7 @@ else { // the left most widget ignores this condition - if (cursorNav && browse == null && !frame.isLeftMostWidget(this)) + if (cursorNav && !isLinkedToBrowse() && !frame.isLeftMostWidget(this)) { // act like a back-tab // the following call should be conditional @@ -1886,7 +1899,7 @@ if (config.appDataPres.getCursorPos() == 0) { // the left most widget ignores this condition - if (cursorNav && browse == null && !frame.isLeftMostWidget(this)) + if (cursorNav && !isLinkedToBrowse() && !frame.isLeftMostWidget(this)) { // act like a back-tab if (!completeEdit()) @@ -1925,7 +1938,7 @@ else { // the right most widget ignores this condition - if (cursorNav && browse == null && !frame.isRightMostWidget(this)) + if (cursorNav && !isLinkedToBrowse() && !frame.isRightMostWidget(this)) { // act like a tab // the following call should be conditional @@ -1939,7 +1952,7 @@ if (cur >= end && (cursorNav || cur >= getText(true).length())) { // the right most widget ignores this condition - if (cursorNav && browse == null && !frame.isRightMostWidget(this)) + if (cursorNav && !isLinkedToBrowse() && !frame.isRightMostWidget(this)) { // act like a tab if (!completeEdit()) @@ -1964,9 +1977,9 @@ } // In-browse HOME and END - else if ((key == Keyboard.KA_HOME || key == Keyboard.KA_END) && browse != null) + else if ((key == Keyboard.KA_HOME || key == Keyboard.KA_END) && isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -2136,9 +2149,9 @@ } // PAGE-UP/PAGE-DOWN action key - else if ((key == Keyboard.KA_PAGE_DOWN || key == Keyboard.KA_PAGE_UP) && browse != null) + else if ((key == Keyboard.KA_PAGE_DOWN || key == Keyboard.KA_PAGE_UP) && isLinkedToBrowse()) { - browse.processKeyEvent(ke); + getBrowse().processKeyEvent(ke); return; } @@ -2957,14 +2970,14 @@ } /** - * Link editor with the Browse widget. - * - * @param browse - * The instance of the Browse to link with. + * Link this fill-in with the BrowseColumn widget. + * + * @param browseColumn + * Parent browse column widget. */ - public void setBrowse(Browse browse) + public void setBrowseColumn(BrowseColumn browseColumn) { - this.browse = browse; + this.browseColumn = browseColumn; } /** @@ -2976,7 +2989,17 @@ */ public boolean isLinkedToBrowse() { - return browse != null; + return browseColumn != null; + } + + /** + * Get associated browse widget, if any. + * + * @return associated browse widget or null if there is no associated browse widget. + */ + protected Browse getBrowse() + { + return browseColumn != null ? browseColumn.getBrowse() : null; } /** @@ -3093,7 +3116,9 @@ */ public boolean isFormatCheck() { - return formatCheck && config.appFormat.isFormatCheck(); + Optional> frame = enclosingFrame(); + return formatCheck && config.appFormat.isFormatCheck() && + (!frame.isPresent() || !frame.get().config().noAutoValidate); } /** @@ -3199,6 +3224,10 @@ FillInConfig c = config(); + if (c.dataType != null && !c.dataType.equals(beforeUpdate.dataType)) + { + clearOverride(); + } setDataType(c.dataType); setTextGroup(c.grouping); @@ -3229,6 +3258,25 @@ } /** + * This method returns the widget that should serve as the source of the triggered event. This widget must + * always be a non-virtual widget. The method may also return {@code null} which prevents triggering of the + * legacy event.

+ * If this fill-in is an editing fill-in for a browse column, that browse column widget is returned. + * + * @param evtCode + * The event being triggered. + * @param isKey + * Whether the event being triggered is a real key. + * + * @return see above. + */ + public Widget getTriggerWidget(int evtCode, boolean isKey) + { + return isLinkedToBrowse() ? browseColumn.getTriggerWidget(evtCode, isKey) : + super.getTriggerWidget(evtCode, isKey); + } + + /** * Create the left-edge scroller. By default, this is a {@link NoOpScroller no-op}. * * @return See above. @@ -3448,7 +3496,7 @@ drawLine = widget.drawLine; deltaWidth = widget.deltaWidth; autoFormat = widget.autoFormat; - browse = null; + browseColumn = null; forceLeft = widget.forceLeft; skipActivation = widget.skipActivation; inAutoReturn = widget.inAutoReturn; === modified file 'src/com/goldencode/p2j/ui/client/FocusManager.java' --- src/com/goldencode/p2j/ui/client/FocusManager.java 2020-01-26 18:12:38 +0000 +++ src/com/goldencode/p2j/ui/client/FocusManager.java 2021-01-27 17:05:13 +0000 @@ -2,7 +2,7 @@ ** Module : FocusManager.java ** Abstract : ** -** Copyright (c) 2010-2020, Golden Code Development Corporation. +** Copyright (c) 2010-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SIY 20100915 Created initial version @@ -53,6 +53,7 @@ ** focus handling. ** SVL 20200115 EntryEvent is used instead of KeyInput for an entry event. ** 030 VVT 20200203 Javadoc fixes. +** CA 20210127 Fixed a NPE when changing focus to the menubar via ALT. */ /* @@ -1518,9 +1519,9 @@ return false; } - Frame oldFrame = (Frame) oldFocus.enclosingFrame().get(); - Frame inFrame = (Frame) inFocus.enclosingFrame().get(); - if (oldFrame != inFrame) + Frame oldFrame = (Frame) oldFocus.enclosingFrame().orElse(null); + Frame inFrame = (Frame) inFocus.enclosingFrame().orElse(null); + if (oldFrame != null && inFrame != null && oldFrame != inFrame) { // if different frames, send LEAVE to old frame if (!tc.sendLeave(oldFrame, inFocus)) === modified file 'src/com/goldencode/p2j/ui/client/Frame.java' (properties changed: -x to +x) --- src/com/goldencode/p2j/ui/client/Frame.java 2020-10-04 20:14:00 +0000 +++ src/com/goldencode/p2j/ui/client/Frame.java 2021-01-23 18:01:03 +0000 @@ -2,7 +2,7 @@ ** Module : Frame.java ** Abstract : Frame implementation for CHUI ** -** Copyright (c) 2005-2020, Golden Code Development Corporation. +** Copyright (c) 2005-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 EVL 20050324 @20506 Creation based on CHARVA JFrame class. @@ -674,6 +674,12 @@ ** HC 20201004 Performance optimizations of server-client config state ** synchronization. ** GES 20201004 sizeChanged is a local variable, not a member of config. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** CA 20201104 During config update, the layout will not be re-calculated even if the frame is +** realized. So don't alter the frame widget tree (side-labels and others). +** CA 20201201 In case of a TEXT:READ-ONLY=true and TEXT:HIDDEN=true widget, the ENABLE statement +** will not make it visible. +** CA 20210123 getFirstEnabledWidget must use the tab order and not the z-order. */ /* @@ -1273,7 +1279,7 @@ { for (Widget comp : getContentPane().widgets()) { - if (comp instanceof Label) + if (comp instanceof ClientDrivenLabel) continue; if (comp instanceof Skip) continue; @@ -2666,32 +2672,35 @@ { savedTabOrder = null; - // remove all labels (they we'll be re-added by the layout) - Widget[] widgets = getContentPane().widgets(); - for (Widget widget : widgets) - { - if (widget instanceof Label) - { - widget.destroy(); - screen().getRegistry().removeWidget(UiUtils.getWidgetId(widget)); - } - } - - // remove the down-body widgets before doLayout() is called, frame's - // layout manager has no idea about the down-body and would give unexpected - // results, note that valid down-body is required in the frame initialization - // steps before setVisible() is called - if (down && downBody != null) - { - for (int i = 1; i < downBody.length; i++) - { - for (Widget w : downBody[i]) + if (!configUpdateActive) + { + // remove all client-managed labels (they we'll be re-added by the layout) + Widget[] widgets = getContentPane().widgets(); + for (Widget widget : widgets) + { + if (widget instanceof ClientDrivenLabel) { - getContentPane().remove(w); + widget.destroy(); + screen().getRegistry().removeWidget(UiUtils.getWidgetId(widget)); } } - downBody = null; + // remove the down-body widgets before doLayout() is called, frame's + // layout manager has no idea about the down-body and would give unexpected + // results, note that valid down-body is required in the frame initialization + // steps before setVisible() is called + if (down && downBody != null) + { + for (int i = 1; i < downBody.length; i++) + { + for (Widget w : downBody[i]) + { + getContentPane().remove(w); + } + } + + downBody = null; + } } config().realized = true; @@ -7024,14 +7033,14 @@ for (int i = 0; i < c.length; i++) { - if ((c[i] instanceof Label) || + if ((c[i] instanceof ClientDrivenLabel) || (UiUtils.hasConfig(c[i]) && c[i].isHeader()) || (c[i] instanceof Space) || (UiUtils.getConfig(c[i]) == null)) { // all these widgets do not belong to down frame body - if (c[i] instanceof Label && isSideLabels()) + if (c[i] instanceof ClientDrivenLabel && isSideLabels()) continue; if (others != null) @@ -7468,16 +7477,16 @@ LabeledWidget lwsrc = (LabeledWidget) source; boolean sideLabels = isSideLabels(); - - if (lwsrc.getLabelInstance() != null && sideLabels) + + ClientDrivenLabel lblSrc = (ClientDrivenLabel) lwsrc.getLabelInstance(); + if (lblSrc != null && sideLabels) { - Label lblSrc = lwsrc.getLabelInstance(); - Label label = lw.getLabelInstance(); + ClientDrivenLabel label = (ClientDrivenLabel) lw.getLabelInstance(); if (label == null) { WidgetId nextId = dest.config().id.createId(WidgetId.nextID()); - label = screen().getFactory().createLabel(nextId, - lwsrc.getLabelInstance(), + label = screen().getFactory().createLabel(nextId, + lblSrc, this.config.id.asInt()); screen().getRegistry().addWidget((Widget) label); @@ -7519,23 +7528,6 @@ } /** - * Move the cursor to the component in the nearest row below this widget. - *

- * See {@link FrameFocusTransferManager#moveBelowWidget(Widget, int)} for - * more details. - * - * @param widget - * widget to check - * - * @param validCursorPos - * cursor offset within the widget - */ - public void moveBelowWidget(Widget widget, int validCursorPos) - { - focusTransferManager.moveBelowWidget(widget, validCursorPos); - } - - /** * Check if the specified widget is the left-most component in the current * screen row (not the same as the current record which may wrap). *

@@ -7544,7 +7536,7 @@ * * @param widget * widget to check - * + * * @return true if this component is the left-most component * in the row. */ @@ -7554,6 +7546,23 @@ } /** + * Move the cursor to the component in the nearest row below this widget. + *

+ * See {@link FrameFocusTransferManager#moveBelowWidget(Widget, int)} for + * more details. + * + * @param widget + * widget to check + * + * @param validCursorPos + * cursor offset within the widget + */ + public void moveBelowWidget(Widget widget, int validCursorPos) + { + focusTransferManager.moveBelowWidget(widget, validCursorPos); + } + + /** * Check if the specified widget is the right-most component in the current * screen row (not the same as the current record which may wrap). *

@@ -7663,12 +7672,11 @@ public Widget getFirstEnabledWidget() { // find the first enabled widget of this frame - Widget[] widgets = getContentPane().widgets(); - Widget comp = null; + List> widgets = getContentPane().getTabItemList(); - for (int i = 0; i < widgets.length; i++) + for (Widget comp : widgets) { - comp = getField(widgets[i]); + comp = getField(comp); if (!comp.isVisible() || !comp.isEnabled()) { continue; @@ -7718,6 +7726,11 @@ { comp = thinClient.getWidget(widgetId[i]); + if (comp.ignoreEnable()) + { + continue; + } + comp = getField(comp); // even non-focusable widgets may need to be enabled; @@ -7917,6 +7930,11 @@ { comp = getField(list[i]); + if (comp.ignoreEnable()) + { + continue; + } + if (thinClient.isChui() && !UiUtils.isFocusable(comp)) { // GUI widgets can be enabled even if not focusable. They are still accessible by @@ -7986,7 +8004,7 @@ for (Widget component : cmp) { - if (component instanceof Label && + if (component instanceof ClientDrivenLabel && rect.intersects(new Rectangle(component.screenLocation(), component.dimension(), component.screen().coordinates().baseUnits()))) @@ -8174,7 +8192,7 @@ // determine presence of headers and drawing mode for (int i = 0; i < c.length; i++) { - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) continue; if (UiUtils.hasConfig(c[i])) === modified file 'src/com/goldencode/p2j/ui/client/Label.java' (properties changed: +x to -x) --- src/com/goldencode/p2j/ui/client/Label.java 2020-03-16 17:30:47 +0000 +++ src/com/goldencode/p2j/ui/client/Label.java 2020-10-24 22:32:57 +0000 @@ -1,814 +1,171 @@ /* -** Module : Label.java -** Abstract : Label widget implementation. -** -** Copyright (c) 2005-2020, Golden Code Development Corporation. -** -** -#- -I- --Date-- --JPRM-- ----------------------------Description------------------------------ -** 001 SVG 20050609 @21452 Initial version. -** 003 SIY 20050831 @22506 Refactoring (see @@22394-22409). -** 004 SIY 20050907 @22640 Fixed use of @inheritDoc tag in javadoc. -** 005 SIY 20050926 @22842 Using ColorTable for drawing. -** 006 SIY 20060209 @24471 Updated to accommodate changes in xxxConfig -** classes. Minor cleanups. -** 007 SIY 20060323 @25222 Added copy constructor. -** 008 SIY 20060615 @27239 Do not draw label if its parent is null. -** 009 SIY 20060824 @28853 Removed getP2JType() method. -** 010 SIY 20060908 @29380 Fixed drawing. -** 011 GES 20061013 @30401 Added a delimiter flag and changed drawing in redirected output mode -** (if we are marked as a delimiter) to avoid the output of the line -** itself (instead we only output a blank line). -** 012 SIY 20070214 @32128 Renamed getDelimiter() to isDelimiter() to match common naming -** convention for boolean properties. -** 013 SIY 20071004 @35366 Rolled back #011. -** 014 NVS 20080401 @37778 Added isFocusable() method implementation. -** 015 SIY 20080401 @38798 Added support for quirks label positioning in ATTR-SPACE handling. -** 016 SIY 20100111 @44522 Fixed a rare case when drawing of empty space instead of label is -** necessary. -** 017 SIY 20100619 Switching to own implementation of character UI. -** 018 SIY 20100804 Changes in imports, some refactorings. -** 019 SIY 20100822 Refactored AttributeContainer support. -** 020 SIY 20100915 Renamed base class. -** 021 SIY 20110112 Changes related to new color handling scheme. -** 022 SIY 20111101 Updated imports. -** 023 SIY 20111202 Refactored and moved from CHUI package. -** 024 CA 20140805 Added generics to force the abstract widget classes to not use -** implementation-specific code: the abstract widgets will have access -** only to the base APIs provided by the OutputManager. -** 025 HC 20140911 GUI coordinate support - data types refactoring. -** 026 CA 20140926 Added shared widget configuration support, to allow GUI/ChUI -** concrete implementation for the same widget ID. Refactored the -** widget configuration classes: all fields were made public; these -** classes need to be as dumb as possible, all logic related to -** setting/getting a certain field should be at the widget or at the -** caller. Refs #2254 -** 027 HC 20141021 Implemented window move/position support. -** 028 CA 20141220 Implemented drawCaret, as is a no-op for LABEL widget. -** 029 CA 20150105 Changes for SIDE-LABEL-HANDLE runtime support. -** 030 CA 20150114 Fixed support for changing the ROW/COLUMN attrs from server-side, -** for a side-label. -** 031 CA 20150127 If label text is changed, invalidate the location. -** 032 HC 20150128 Added support for frame widget position changes induced by -** setting ROW/COL/X/Y attributes. -** 033 HC 20150214 Improved positioning of side labels. -** 034 CA 20150222 Fixed label's COLUMN and WIDTH-CHARS attributes (in ChUI). -** 035 CA 20150323 Pass the frame ID when creating a label (to properly resolve the font -** in GUI). -** When linking a label, preserve the label's color attributes, too. -** 036 HC 20150323 Removed business logic from config classes. -** 037 CA 20150406 Track the label's side widget (if this is a side-label), as in GUI -** the side-label implicitly follows the widget's height. -** 038 CA 20150519 The label's font needs to be copied, when the label is replaced. -** 039 CA 20150722 Changes to allow frame layout to be done only once, on the frame -** definition. Subsequent label changes are managed by postprocessConfig. -** 040 CA 20150728 Added getter for sideWidget field. -** 041 CA 20151112 "label does not fit in frame" error message is shown only if the label -** changes. -** 042 EVL 20160413 Label can not have '&' character inside. -** 043 EVL 20160823 Fix for column calculation with side-labeled widgets. -** 044 HC 20160922 Added getLegacyWidget() method. -** 045 HC 20161018 Frame layout improvements. -** 046 CA 20161101 Added support for mnemonic processing. -** 047 EVL 20170724 Direct manipulation is not valid with LABEL widget. -** 048 CA 20171111 Added WIDGET:BGCOLOR-RGB and WIDGET:FGCOLOR-RGB FWD extension -** attributes -** 049 CA 20180502 Fixed COLOR option, COLOR statement and other color-related issues. -** 050 HC 20190823 Changed an error message to a warning. -** 051 HC 20200313 Javadoc fixes. -*/ + ** Module : Label.java + ** Abstract : Interface for label widgets. + ** + ** Copyright (c) 2020, Golden Code Development Corporation. + ** + ** -#- -I- --Date-- --------------------------------Description----------------------------------- + ** 001 HC 20201024 Created initial version. + */ /* -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU Affero General Public License as -** published by the Free Software Foundation, either version 3 of the -** License, or (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU Affero General Public License for more details. -** -** You may find a copy of the GNU Affero GPL version 3 at the following -** location: https://www.gnu.org/licenses/agpl-3.0.en.html -** -** Additional terms under GNU Affero GPL version 3 section 7: -** -** Under Section 7 of the GNU Affero GPL version 3, the following additional -** terms apply to the works covered under the License. These additional terms -** are non-permissive additional terms allowed under Section 7 of the GNU -** Affero GPL version 3 and may not be removed by you. -** -** 0. Attribution Requirement. -** -** You must preserve all legal notices or author attributions in the covered -** work or Appropriate Legal Notices displayed by works containing the covered -** work. You may not remove from the covered work any author or developer -** credit already included within the covered work. -** -** 1. No License To Use Trademarks. -** -** This license does not grant any license or rights to use the trademarks -** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks -** of Golden Code Development Corporation. You are not authorized to use the -** name Golden Code, FWD, or the names of any author or contributor, for -** publicity purposes without written authorization. -** -** 2. No Misrepresentation of Affiliation. -** -** You may not represent yourself as Golden Code Development Corporation or FWD. -** -** You may not represent yourself for publicity purposes as associated with -** Golden Code Development Corporation, FWD, or any author or contributor to -** the covered work, without written authorization. -** -** 3. No Misrepresentation of Source or Origin. -** -** You may not represent the covered work as solely your work. All modified -** versions of the covered work must be marked in a reasonable way to make it -** clear that the modified work is not originating from Golden Code Development -** Corporation or FWD. All modified versions must contain the notices of -** attribution required in this license. -*/ - + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU Affero General Public License as + ** published by the Free Software Foundation, either version 3 of the + ** License, or (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU Affero General Public License for more details. + ** + ** You may find a copy of the GNU Affero GPL version 3 at the following + ** location: https://www.gnu.org/licenses/agpl-3.0.en.html + ** + ** Additional terms under GNU Affero GPL version 3 section 7: + ** + ** Under Section 7 of the GNU Affero GPL version 3, the following additional + ** terms apply to the works covered under the License. These additional terms + ** are non-permissive additional terms allowed under Section 7 of the GNU + ** Affero GPL version 3 and may not be removed by you. + ** + ** 0. Attribution Requirement. + ** + ** You must preserve all legal notices or author attributions in the covered + ** work or Appropriate Legal Notices displayed by works containing the covered + ** work. You may not remove from the covered work any author or developer + ** credit already included within the covered work. + ** + ** 1. No License To Use Trademarks. + ** + ** This license does not grant any license or rights to use the trademarks + ** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks + ** of Golden Code Development Corporation. You are not authorized to use the + ** name Golden Code, FWD, or the names of any author or contributor, for + ** publicity purposes without written authorization. + ** + ** 2. No Misrepresentation of Affiliation. + ** + ** You may not represent yourself as Golden Code Development Corporation or FWD. + ** + ** You may not represent yourself for publicity purposes as associated with + ** Golden Code Development Corporation, FWD, or any author or contributor to + ** the covered work, without written authorization. + ** + ** 3. No Misrepresentation of Source or Origin. + ** + ** You may not represent the covered work as solely your work. All modified + ** versions of the covered work must be marked in a reasonable way to make it + ** clear that the modified work is not originating from Golden Code Development + ** Corporation or FWD. All modified versions must contain the notices of + ** attribution required in this license. + */ package com.goldencode.p2j.ui.client; -import java.util.Objects; - -import com.goldencode.util.*; import com.goldencode.p2j.ui.*; -import com.goldencode.p2j.ui.chui.ThinClient; import com.goldencode.p2j.ui.client.widget.*; /** - * Implementation of label for Progress components. Progress uses TEXT - * components for labels but this seems to be incorrect. TEXT implies - * underlying variable and complex format. + * Interface implemented by widgets that can act as widget labels. */ -public abstract class Label> -extends FixedSizeContainer -implements WidgetWithConfig +public interface Label> +extends Widget { - /** Widget's size which is set by setSize method. */ - private boolean forcedSize = false; - - /** Component attribute container. */ - protected LabelConfig config = null; - - /** Make label aligned to right boundary. */ - protected boolean rightAligned = true; - - /** Display additional line */ - protected boolean underlined = false; - - /** Flag the special case of use as a separator line. */ - private boolean delimiter = false; - - /** Label right shift offset. */ - protected double offset = 0; - - /** If it is true, empty string is displayed.*/ - protected boolean emptyMode = false; - - /** Prepare for disabling empty mode. */ - protected boolean disableEmptyMode = false; - - /** The side-widget associated with this label, if this is a side-label. */ - protected Widget sideWidget = null; - - /** - * Constructs label with given text. - * - * @param id - * The widget's ID. - * @param text - * Initial text. - * @param frameId - * The ID of the frame to which the label belongs, or -1 if unknown at the time of the - * creation. - */ - public Label(WidgetId id, String text, int frameId) - { - config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); - config.frameId = frameId; - - setName(text); - } - - /** - * Create a copy of the label. - * - * @param id - * The widget's ID. - * @param label - * Label to copy parameters from. - * @param frameId - * The ID of the frame to which the label belongs, or -1 if unknown at the time of the - * creation. - */ - public Label(WidgetId id, Label label, int frameId) - { - config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); - config.frameId = frameId; - - setName(label.config.name); - - setUnderlined(label.isUnderlined()); - setRightAligned(label.isRightAligned()); - setWidth(label.width()); - forcedSize = label.forcedSize; - setLocation(label.location()); - setVisible(label.isVisible()); - } - - /** - * Retrieve the widget's width in native units. - * - * @return See above. - */ - protected abstract int nativeWidth(); - - /** - * Retrieve the widget's height in native units. - * - * @return See above. - */ - protected abstract int nativeHeight(); - - /** - * Set the specified widget as this side-label's {@link #sideWidget}. - * - * @param widget - * The side-widget for this label. - */ - public void setSideWidget(Widget widget) - { - this.sideWidget = widget; - updateSize(); - } - - /** - * Get the side-widget associated with this label. - * - * @return The {@link #sideWidget}. - */ - public Widget getSideWidget() - { - return sideWidget; - } - - /** - * Replacement API for the logic which should exist in the widget's c'tor, but the logic - * can't be executed as it depends on the widget being registered into the official registry. - *

- * This API is called after the widget is created and added to the registry. - * - * @param id - * The widget's ID. - * @param cfg - * The config used to initialize this widget. - */ - @Override - public void initialize(WidgetId id, LabelConfig cfg) - { - config = ConfigManager.getInstance().resolveWidgetConfig(id, cfg); - ConfigManager.getInstance().replaceConfig(config, cfg); - } - - /** - * Create a copy of the label. - * - * @param id - * The widget's ID. - * @param label - * Label to copy parameters from. - */ - public Label(WidgetId id, Label label) - { - config = ConfigManager.getInstance().resolveWidgetConfig(id, LabelConfig.class); - - config.name = label.config.name; - setUnderlined(label.isUnderlined()); - setRightAligned(label.isRightAligned()); - setWidth(label.width()); - forcedSize = label.forcedSize; - setLocation(label.location()); - setVisible(label.isVisible()); - } - - /** - * Provide access to the attributes stored in the widget. - * - * @return Reference to attribute container. - */ - @SuppressWarnings("unchecked") - @Override - public LabelConfig config() - { - return config; - } - - /** - * Marks this label as one used to draw a separator line for header - * widgets. In this case there is no label text but the width is - * positive and the underline attribute is enabled. - * - * @param delimiter - * true to set this as a separator line. - */ - public void setDelimiter(boolean delimiter) - { - this.delimiter = delimiter; - } - - /** - * Gets the flag that marks this label as one used to draw a separator - * line for header widgets. - * - * @return true if this label is a separator line. - */ - public boolean isDelimiter() - { - return delimiter; - } - - /** - * Draws text. Screen value is left justified or padded to the right bound - * with spaces. Non first lines are filled with spaces. - */ - @Override - public abstract void draw(); - - /** - * Sets position of terminal's cursor. Really does nothing because this - * widget will never get an input focus. - */ - @Override - public void drawCaret() - { - } - - /** - * Sets new widget's size. After call to this method the widget will - * not change size automatically accordingly to screen value changes. - * - * @param width - * New widget width. - * @param height - * New widget height. - */ - @Override - public void setSize(double width, double height) - { - forcedSize = true; - super.setSize(width, height); - } - - /** - * Set widget width. - * - * @param width - * New widget width. - */ - @Override - public void setWidth(double width) - { - forcedSize = true; - super.setWidth(width); - } - - /** - * Set widget height. - * - * @param height - * New widget height. - */ - @Override - public void setHeight(double height) - { - forcedSize = true; - super.setHeight(height); - } - - /** - * This component will not receive an input focus when Tab or Shift-Tab is - * pressed. - * - * @return Always false. - */ - @Override - public boolean focusTraversable() - { - return false; - } - - /** - * Checks whether this attribute container can receive focus generally. - * - * @return true if this container can be focused under - * certain conditions. false if, and only if, this - * attribute container can never receive focus. - */ - @Override - public boolean isFocusable() - { - return false; - } - - /** - * Synchronizes the actual size with the default size. - */ - protected void updateSize() - { - if (forcedSize) - return; - - int nativeWith = nativeWidth(); - int nativeHeight = nativeHeight(); - - CoordinatesConversion cc = screen().coordinates(); - double width = cc.widthFromNative(nativeWith); - double height = cc.heightFromNative(nativeHeight); - - // call super to avoid setting forcedSize to true - super.setSize(width, height); - - // synchronize the new size with server - BaseConfig c = config(); - c.widthChars = width; - c.heightChars = height; - } - - /** - * Set underlining mode. - * - * @param b - * New value for the mode. - */ - public void setUnderlined(boolean b) - { - underlined = b; - updateSize(); + /** + * Process this label so that it's linked with the server-side widget represented by the + * serverSideId. + * + * @param frameId + * The ID of the frame to which this label belongs. + * @param serverSideId + * The server-side ID of this label. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + default void linkTo(int frameId, int serverSideId) + { + // no-op by default } /** * Get current value of the underlining flag. - * + * * @return true if label is underlined. */ - public boolean isUnderlined() - { - return underlined; - } - - /** - * Get current value of the alignment. - * - * @return true if right alignment is active. - */ - public boolean isRightAligned() - { - return rightAligned; - } - + default boolean isUnderlined() + { + return false; + } + /** * Activate or deactivate right alignment of the text. - * + * * @param rightAligned * true activate right alignment, * false- deactivate. */ - public void setRightAligned(boolean rightAligned) - { - this.rightAligned = rightAligned; - } - - /** - * Sets the name of the label. - * - * @param name - * New name of the component. - */ - public void setName(String name) - { - config.name = name; - - //Reset the forcedSize flag so updateSize() can properly adjust the size. - forcedSize = false; - updateSize(); - } - - /** - * Get widget name. - * - * @return widget name or empty string if is not set. - */ - @Override - public String name() - { - String name = super.name(); - return name == null ? "" : name; - } - - /** - * Set extra offset for label. - * - * @param offset - * Size of the shift. - */ - public void setOffset(double offset) - { - this.offset = Coordinate.scale(offset); + default void setRightAligned(boolean rightAligned) + { + // no-op by default + } + + /** + * Enable empty mode. + */ + default String label() + { + return name(); + } + + default void setEmptyModeOn() + { + // no-op by default } /** * Disable empty label mode. Note that actual disabling happens only during * next {@link #draw()} call. */ - public void setEmptyModeOff() - { - disableEmptyMode = true; - } - - /** - * Enable empty mode. - */ - public void setEmptyModeOn() - { - emptyMode = true; - } - - /** - * Process this label so that it's linked with the server-side widget represented by the - * serverSideId. - * - * @param frameId - * The ID of the frame to which this label belongs. - * @param serverSideId - * The server-side ID of this label. - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void linkTo(int frameId, int serverSideId) - { - if (serverSideId == -1) - { - return; - } - - BaseConfig lblCfg = config(); - WidgetId oldId = lblCfg.id; - WidgetId newId = oldId.createId(serverSideId); - - ConfigManager cm = ConfigManager.getInstance(); - BaseConfig bc = cm.getActiveConfig(newId); - - if (bc != null) - { - Point loc = location(); - - double x = loc.x; - double y = loc.y; - boolean setLoc = false; - - double assignedCol = ConfigHelper.getAssignedColumn(bc); - double assignedRow = ConfigHelper.getAssignedRow(bc); - - if (assignedCol != BaseConfig.INV_COORD) - { - x = assignedCol; - setLoc = true; - } - - if (assignedRow != BaseConfig.INV_COORD) - { - y = assignedRow; - setLoc = true; - } - - if (setLoc) - { - setLocation(x, y); - } - - // inherit width, but only when it is not already set on this - if (bc.widthChars != -1 && width() == -1) - { - setWidth(bc.widthChars); - lblCfg.widthChars = bc.widthChars; - } - - lblCfg.align = bc.align; - - // restore the colors - copyColors(bc, lblCfg); - lblCfg.dcolor = bc.dcolor; - lblCfg.pfcolor = bc.pfcolor; - lblCfg.sysbgcolor = bc.sysbgcolor; - lblCfg.sysfgcolor = bc.sysfgcolor; - - // restore the font - lblCfg.font = bc.font; - } - - ClientConfigManager mgr = ConfigManager.getInstance(); - - // switch the internal ID for this label (from virtual to real) - mgr.switchId(oldId, newId); - - // mark as dirty all configs which are not the same as what the server has... - mgr.trackAllChanges(lblCfg); - - OutputManager.instance().getRegistry().addWidget((Widget) this); - } - - /** - * Update the {@link #visible} flag in the configuration and in the widget's state. - *

- * If the widget has just been realized and its width has not yet been set, it will be computed - * now. - * - * @param visible - * The widget's visible state. - */ - @Override - public void _setVisible(boolean visible) - { - boolean oldRealized = config.realized; - - super._setVisible(visible); - - if (!oldRealized && config.realized && config.widthChars == -1) - { - config.widthChars = width(); - } - } - - /** - * Get the state of the {@link #forcedSize} flag. - * - * @return See above. - */ - public boolean isForcedSize() - { - return forcedSize; - } - - /** - * Set the {@link #forcedSize} flag to the specified state. - * - * @param forcedSize - * The new state for the {@link #forcedSize} flag. - */ - public void setForcedSize(boolean forcedSize) - { - this.forcedSize = forcedSize; - } - - /** - * The method is called after the configuration associated with - * the implementor changes. The parameter points to a reference - * representing the original configuration state. This reference - * can be used to detect what configuration fields changed and so - * optimize any processing related to the configuration change. - * - * @param beforeUpdate - * Config reference capturing the config state before - * the owner's config was modified. - */ - @Override - public void afterConfigUpdate(LabelConfig beforeUpdate) - { - super.afterConfigUpdateBase(beforeUpdate); - - LabelConfig c = config(); - - setWidth(c.widthChars); - - // name changed? - if (!Objects.equals(((BaseConfig) beforeUpdate).name, c.name)) - { - //reset the forcedSize flag so updateSize() can properly adjust the size. - forcedSize = false; - updateSize(); - } - } - - /** - * Additional processing required when widget configuration is updated AFTER the frame layout - * has been performed. - */ - @Override - public void postprocessConfig() - { - super.postprocessConfig(); - - if (sideWidget != null) - { - double newLblWidth = 0; - double oldLblWidth = 0; - boolean labelChanged = false; - if (sideWidget instanceof LabeledDataContainer) - { - String newLabel = ((LabeledDataContainer) sideWidget).getLabelText() + ": "; - labelChanged = !newLabel.equals(config.name); - - if (labelChanged) - { - newLblWidth = ZeroColumnLayout.labelWidth((LabeledWidget) sideWidget, newLabel); - oldLblWidth = ZeroColumnLayout.labelWidth((LabeledWidget) sideWidget, - config.name); - } - } - - // check the left border - if (labelChanged) - { - double newColumn = Coordinate.scale(ConfigHelper.getEffectiveColumn(config) + - oldLblWidth - newLblWidth); - - if (newColumn < 0) - { - Frame fr = (Frame) UiUtils.locateFrame(this); - - StringBuilder err = new StringBuilder(); - - if (screen().isChui()) - { - err.append("**Unable to set SCREEN-VALUE. ") - .append("LITERAL widget does not fit in FRAME ") - .append(fr.shortName()).append(". (4054)"); - } - else - { - err.append("**All or part of LITERAL widget is being placed outside of FRAME ") - .append(fr.shortName()) - .append(" by setting SCREEN-VALUE. (5905)"); - } - - ThinClient.getInstance().displayWarningMessage(err.toString()); - - // no need to restore the label string - } - else - { - Point loc = location(); - config.column = newColumn; - setLocation(newColumn, loc.y); - - if (sideWidget.config() instanceof ControlConfig) - { - // setName will update the size, too - setName(((ControlConfig) sideWidget.config()).label + ": "); - } - } - } - } - } - - /** - * The method returns the legacy widget representing this widget. - * - * @return A non-wirtual widget reference, or null if none found. - */ - @Override - public Widget getLegacyWidget() - { - return getSideWidget(); - } - - /** - * Additional processing required when widget configuration is updated AFTER the frame layout - * has been performed. - * + default void setEmptyModeOff() + { + // no-op by default + } + + /** + * Additional processing required when widget configuration is updated AFTER the frame layout + * has been performed. + * * @param widget * The widget to which this side-labe is attached. */ - protected void postprocessConfig(LabeledWidget widget) + default void postprocessConfig(LabeledWidget widget) { - // check the frame first - this call is only for non-side-labels - Frame frame = (Frame) UiUtils.locateFrame((Widget) widget); - if (frame.isSideLabels()) - { - return; - } - - String label = ZeroColumnLayout.getLabel(widget, frame.isSideLabels(), frame.hasHeaders()); - if (label != null && !label.equals(this.name())) - { - // TODO: if new width is greater than frame width, then the width remains as the - // current label's width - and the new label is trimmed to this width - setName(label); - } + // no-op by default } /** - * Check the direct manipulation capability of the widget. LABEL widget does not allow direct - * manipulation. - * - * @return TRUE if the widget is able to be directly manipulated, - * FALSE otherwise. + * Gets the flag that marks this label as one used to draw a separator + * line for header widgets. + * + * @return true if this label is a separator line. */ - @Override - public boolean isDirectManipulable() + default boolean isDelimiter() { return false; } -} \ No newline at end of file + + /** + * Set container width. + * + * @param width + * Width to set. + */ + void setWidth(double width); + + /** + * Get externalizable widget config which can be used to restore widget + * state after via network. + * Note that only very limited set of widgets actually have config. + * + * @return Reference to widget config. + */ + BaseConfig config(); +} === modified file 'src/com/goldencode/p2j/ui/client/LabeledDataContainer.java' --- src/com/goldencode/p2j/ui/client/LabeledDataContainer.java 2020-09-19 17:47:54 +0000 +++ src/com/goldencode/p2j/ui/client/LabeledDataContainer.java 2020-10-24 22:32:57 +0000 @@ -42,6 +42,7 @@ ** 031 HC 20200313 Javadoc fixes. ** 032 RFB 20200918 Protect nativeHeight() and nativeWidth() from config being null so some toString() ** calls can be made prior to opening frame scope (#4861). +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -321,7 +322,18 @@ super.setVisible(visible); if (sideLabel != null) + { sideLabel.setVisible(visible); + } + else if (config.sideLabelId != -1) + { + // the case when TEXT widget is assigned to SIDE-LABEL-HANDLE + Widget w = ThinClient.getInstance().getWidget(config.sideLabelId); + if (w != null) + { + w.setVisible(visible); + } + } } /** === modified file 'src/com/goldencode/p2j/ui/client/MenuItem.java' --- src/com/goldencode/p2j/ui/client/MenuItem.java 2020-03-16 17:30:47 +0000 +++ src/com/goldencode/p2j/ui/client/MenuItem.java 2021-01-28 17:31:08 +0000 @@ -2,7 +2,7 @@ ** Module : MenuItem.java ** Abstract : MenuItem abstract client implementation ** -** Copyright (c) 2015-2020, Golden Code Development Corporation. +** Copyright (c) 2015-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- -----------------------------Description----------------------------- ** 001 VIG 20150116 Created initial version. @@ -29,6 +29,7 @@ ** MENU-BAR. Fixed menu item subtype checks. ** CA 20180530 Fixed menubar components sensitivity when menubar is disabled. ** 019 HC 20200313 Javadoc fixes. +** 020 CA 20210128 Fixed problems with navigating a menubar via ALT and LEFT/RIGHT/UP/DOWN keys. */ /* ** This program is free software: you can redistribute it and/or modify @@ -371,6 +372,12 @@ parentSub.hideBody(false); } } + + Widget focus = UiUtils.getCurrentFocus(); + if (Menu.isMenubarElement(focus) && focus instanceof SubMenu) + { + ((SubMenu) focus).setInitialFocus(); + } return; } === modified file 'src/com/goldencode/p2j/ui/client/NativeRectangle.java' --- src/com/goldencode/p2j/ui/client/NativeRectangle.java 2020-03-31 19:34:28 +0000 +++ src/com/goldencode/p2j/ui/client/NativeRectangle.java 2020-10-19 15:27:22 +0000 @@ -4,7 +4,7 @@ ** ** Copyright (c) 2014-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description---------------------------------------- ** 001 HC 20140911 Created initial version. ** 002 HC 20150508 Improvements to GUI window scrolling support. ** 003 HC 20150913 Added toString(). @@ -12,6 +12,9 @@ ** case if this object or its parameter are represented by empty rectangles. ** 005 VVT 20200207 The equals() method now overloads Object.equal(). ** 006 HC 20200331 Added dimension() method. +** 007 CA 20201019 Cache the hash value. +** Added neighbour() API, which checks if two rectangles are neighbours (and can be merged +** into a larger rectangle, the exact union of the two. */ /* ** This program is free software: you can redistribute it and/or modify @@ -88,6 +91,9 @@ /** Rectangle top row. */ private final int top; + /** The cached hash value. */ + private final int hash; + /** * Constructor. * @@ -106,6 +112,8 @@ this.left = left; this.bottom = bottom; this.right = right; + + this.hash = hash(); } /** @@ -122,6 +130,8 @@ left = topleft.x; bottom = top + size.height - 1; right = left + size.width - 1; + + this.hash = hash(); } /** @@ -136,6 +146,8 @@ left = rect.left(); bottom = rect.bottom(); right = rect.right(); + + this.hash = hash(); } /** @@ -220,10 +232,7 @@ } NativeRectangle rect = (NativeRectangle) o; - return top == rect.top && - bottom == rect.bottom && - left == rect.left && - right == rect.right; + return top == rect.top && bottom == rect.bottom && left == rect.left && right == rect.right; } /** @@ -234,13 +243,7 @@ @Override public int hashCode() { - int r = 17; - r = 37 * r + top; - r = 37 * r + bottom; - r = 37 * r + left; - r = 37 * r + right; - - return r; + return hash; } /** @@ -268,14 +271,10 @@ if (right < rect.left) return null; - return new NativeRectangle((top > rect.top) ? top - : rect.top, - (left > rect.left) ? left - : rect.left, - (bottom < rect.bottom) ? bottom - : rect.bottom, - (right < rect.right) ? right - : rect.right); + return new NativeRectangle((top > rect.top) ? top : rect.top, + (left > rect.left) ? left : rect.left, + (bottom < rect.bottom) ? bottom : rect.bottom, + (right < rect.right) ? right : rect.right); } /** @@ -293,15 +292,11 @@ return false; } - if (rect.left > right) - return false; - if (rect.right < left) - return false; - if (rect.top > bottom) - return false; - if (rect.bottom < top) - return false; - + if (rect.left > right || rect.right < left || rect.top > bottom || rect.bottom < top) + { + return false; + } + return true; } @@ -388,14 +383,44 @@ return new NativeRectangle(this); } - return new NativeRectangle((top < rect.top) ? top - : rect.top, - (left < rect.left) ? left - : rect.left, - (bottom > rect.bottom) ? bottom - : rect.bottom, - (right > rect.right) ? right - : rect.right); + return new NativeRectangle((top < rect.top) ? top : rect.top, + (left < rect.left) ? left : rect.left, + (bottom > rect.bottom) ? bottom : rect.bottom, + (right > rect.right) ? right : rect.right); + } + + /** + * Check if this an the other rectangle are neighbours. Two rectangles are neighbours if they can be + * 'glued' together by an edge, thus forming their exact union rectangle. + * + * @param rect + * The other rectangle to check. + * + * @return See above. + */ + public boolean neighbour(NativeRectangle rect) + { + if (this.top == rect.bottom + 1 && this.left == rect.left && this.right == rect.right) + { + return true; + } + + if (this.bottom == rect.top - 1 && this.left == rect.left && this.right == rect.right) + { + return true; + } + + if (this.left == rect.right + 1 && this.top == rect.top && this.bottom == rect.bottom) + { + return true; + } + + if (this.right == rect.left - 1 && this.top == rect.top && this.bottom == rect.bottom) + { + return true; + } + + return false; } /** @@ -490,7 +515,22 @@ @Override public String toString() { - return "NativeRectangle[top=" + top + ", left=" + left + ", bottom=" + bottom + ", right=" - + right + "]"; + return "NativeRectangle[top=" + top + ", left=" + left + ", bottom=" + bottom + ", right=" + right + "]"; + } + + /** + * Compute the hash value, so that it can be cached. + * + * @return See above. + */ + private int hash() + { + int r = 17; + r = 37 * r + top; + r = 37 * r + bottom; + r = 37 * r + left; + r = 37 * r + right; + + return r; } } \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/client/OutputManager.java' --- src/com/goldencode/p2j/ui/client/OutputManager.java 2018-12-12 23:15:56 +0000 +++ src/com/goldencode/p2j/ui/client/OutputManager.java 2020-10-09 17:55:08 +0000 @@ -543,7 +543,7 @@ * * @return See above. */ - public abstract WidgetRegistry> getRegistry(); + public abstract > WidgetRegistry getRegistry(); /** * Check if the given key can raise the STOP condition. === modified file 'src/com/goldencode/p2j/ui/client/ScreenBitmap.java' --- src/com/goldencode/p2j/ui/client/ScreenBitmap.java 2018-06-19 17:08:24 +0000 +++ src/com/goldencode/p2j/ui/client/ScreenBitmap.java 2020-10-19 15:27:22 +0000 @@ -2,16 +2,15 @@ ** Module : ScreenBitmap.java ** Abstract : Provide clipping behavior for drawing. ** -** Copyright (c) 2008-2018, Golden Code Development Corporation. +** Copyright (c) 2008-2020, Golden Code Development Corporation. ** -** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** -#- -I- --Date-- ---------------------------------------Description---------------------------------------- ** 001 SIY 20100629 Created initial version ** 002 SIY 20100804 Changes in imports, some refactorings. ** 003 SIY 20100903 Fixed javadoc. ** 004 SIY 20111204 Updated imports. -** 005 CA 20140729 Removed the matrix bitmap: instead, the activated rectangles are kept in a -** list, to allow decimal coordinates and a more dynamic approach on the screen's -** dimensions. +** 005 CA 20140729 Removed the matrix bitmap: instead, the activated rectangles are kept in a list, to allow +** decimal coordinates and a more dynamic approach on the screen's dimensions. ** 006 CA 20140806 Changes to allow resizable windows and clipping. ** 007 HC 20140911 GUI coordinate support - data types refactoring. ** 008 CA 20150406 Removed a comment - the bitmap is always kept in native rectangles. @@ -20,8 +19,9 @@ ** clip rectangle. ** 011 SBI 20171208 Fixed to discard empty rectangles that cause drawing artifacts. ** 012 CA 20180619 Fixed getCopy() - hasRectangles flag must be copied, too. -** Fixed a bug in resetScreen(), where the bitmap rectangles were not being -** cleared. +** Fixed a bug in resetScreen(), where the bitmap rectangles were not being cleared. +** 013 CA 20201019 Optimized getClippings - this reduces the number of returned rectangles, by merging all +** neighbour rectangles, and eliminating overlapping rectangles. */ /* ** This program is free software: you can redistribute it and/or modify @@ -304,7 +304,7 @@ return; } - List work = new ArrayList<>(); + List work = new LinkedList<>(); if (state) { @@ -1022,22 +1022,124 @@ */ public List getClippings(NativeRectangle bounds) { - List res = new ArrayList<>(); - if (!bounds.empty()) - { - for (NativeRectangle r : bitmap) - { - if (r.empty()) + if (bounds.empty()) + { + return Collections.emptyList(); + } + + Set res = new HashSet<>(); + for (NativeRectangle r : bitmap) + { + if (r.empty()) + { + continue; + } + NativeRectangle intersection = r.intersection(bounds); + if (intersection != null && !intersection.empty()) + { + res.add(intersection); + } + } + + if (res.isEmpty()) + { + return Collections.emptyList(); + } + + if (res.size() == 1) + { + return new ArrayList<>(res); + } + + Set removed = new HashSet<>(); + Set added = new HashSet<>(); + boolean changed = false; + do + { + NativeRectangle[] rectangles = res.toArray(new NativeRectangle[res.size()]); + boolean[] touched = new boolean[rectangles.length]; + + changed = false; + for (int i = 0; i < rectangles.length; i++) + { + NativeRectangle nr1 = rectangles[i]; + if (touched[i]) { continue; } - NativeRectangle intersection = r.intersection(bounds); - if (intersection != null && !intersection.empty()) + + for (int j = i + 1; j < rectangles.length; j++) { - res.add(intersection); + NativeRectangle nr2 = rectangles[j]; + if (touched[j]) + { + continue; + } + + if (nr1.contains(nr2)) + { + removed.add(nr2); + + changed = true; + + touched[j] = true; + break; + } + else if (nr2.contains(nr1)) + { + removed.add(nr1); + + changed = true; + + touched[i] = true; + break; + } + else if (nr1.intersects(nr2)) + { + List diff1 = nr1.difference(nr2, BASE_UNIT); + List diff2 = nr2.difference(nr1, BASE_UNIT); + NativeRectangle intersection = nr1.intersection(nr2); + + removed.add(nr1); + removed.add(nr2); + added.addAll(diff1); + added.addAll(diff2); + added.add(intersection); + + changed = true; + + touched[i] = true; + touched[j] = true; + break; + } + else if (nr1.neighbour(nr2)) + { + removed.add(nr1); + removed.add(nr2); + added.add(nr1.union(nr2)); + + changed = true; + + touched[i] = true; + touched[j] = true; + break; + } } } + + if (!removed.isEmpty()) + { + res.removeAll(removed); + removed.clear(); + } + if (!added.isEmpty()) + { + res.addAll(added); + added.clear(); + } } - return res; + while (changed); + + return new ArrayList<>(res); } } === modified file 'src/com/goldencode/p2j/ui/client/SubMenu.java' --- src/com/goldencode/p2j/ui/client/SubMenu.java 2020-10-06 21:12:12 +0000 +++ src/com/goldencode/p2j/ui/client/SubMenu.java 2021-01-28 17:31:08 +0000 @@ -2,7 +2,7 @@ ** Module : SubMenu.java ** Abstract : SubMenu abstract client implementation ** -** Copyright (c) 2015-2020, Golden Code Development Corporation. +** Copyright (c) 2015-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- -----------------------------Description----------------------------- ** 001 VIG 20150116 Created initial version. @@ -34,6 +34,7 @@ ** 021 VVT 20200203 Code cleanup ** 022 EVL 20200221 Fixed the wrong menu opening when trigger defined for "menu-drop" event. ** 023 HC 20200313 Javadoc fixes. +** 024 CA 20210128 Fixed problems with navigating a menubar via ALT and LEFT/RIGHT/UP/DOWN keys. */ /* ** This program is free software: you can redistribute it and/or modify @@ -108,6 +109,9 @@ /** Menu attribute container. */ protected SubMenuConfig config = null; + /** Flag indicating if the body must be displayed. */ + protected boolean showBody = false; + /** Label with mnemonic info */ private MnemonicInfo label; @@ -427,7 +431,8 @@ { // What if body is already displayed? In this case there will be focused item in // the body, that will process this event. - + + showBody = true; showBody(); if (key != Keyboard.SE_MENU_DROP) { @@ -656,6 +661,16 @@ { return super.isEnabled() && (!Menu.isMenubarElement(this) || parent().isEnabled()); } + + /** + * Get the state of the {@link #showBody} flag. + * + * @return See above. + */ + public boolean isShowBody() + { + return showBody; + } /** * Creates new MnemonicInfo instance. @@ -721,8 +736,16 @@ */ protected void setInitialFocus() { + if (!showBody) + { + return; + } + Widget firstChild = getFirstFocusableItem(); - + if (firstChild == null) + { + return; + } FocusManager.setInitialFocus(firstChild); } === modified file 'src/com/goldencode/p2j/ui/client/TempContainer.java' --- src/com/goldencode/p2j/ui/client/TempContainer.java 2018-08-13 12:55:57 +0000 +++ src/com/goldencode/p2j/ui/client/TempContainer.java 2020-10-24 22:32:57 +0000 @@ -2,7 +2,7 @@ ** Module : TempContainer.java ** Abstract : Temporary container used for layout processing. ** -** Copyright (c) 2010-2017, Golden Code Development Corporation. +** Copyright (c) 2010-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SIY 20100915 Created initial version @@ -24,6 +24,7 @@ ** 010 HC 20150831 Scrolling support overhaul. ** 011 CA 20151123 Added APIs to place a widget on a specified location. ** 012 HC 20160203 Improved runtime support of FRAME:VIRTUAL* attributes. +** 013 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -129,7 +130,7 @@ for (int i = 0; i < c.length; i++) { - if (c[i] instanceof Label && ((Label) c[i]).isDelimiter()) + if (c[i] instanceof ClientDrivenLabel && ((Label) c[i]).isDelimiter()) { addDirectly(c[i]); } === modified file 'src/com/goldencode/p2j/ui/client/Text.java' (properties changed: -x to +x) --- src/com/goldencode/p2j/ui/client/Text.java 2020-01-16 22:00:48 +0000 +++ src/com/goldencode/p2j/ui/client/Text.java 2020-12-01 12:59:56 +0000 @@ -2,7 +2,7 @@ ** Module : Text.java ** Abstract : Java standard header ** -** Copyright (c) 2005-2019, Golden Code Development Corporation. +** Copyright (c) 2005-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- -----------------------------Description----------------------------- ** 001 SVG 20050609 @21452 Initial version. @@ -38,6 +38,9 @@ ** 022 CA 20171020 Added support for FRAMEs sent to named or unnamed streams, in GUI. ** 023 HC 20191204 Improved emulation of setFocus Win32 system function. Plus related ** changes to focus handling. +** 024 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** CA 20201201 In case of a TEXT:READ-ONLY=true and TEXT:HIDDEN=true widget, the ENABLE statement +** will not make it visible. */ /* ** This program is free software: you can redistribute it and/or modify @@ -103,6 +106,7 @@ */ public abstract class Text> extends FillIn +implements Label { /** Track value assignment. */ protected boolean assigned = false; @@ -203,6 +207,26 @@ { return 0; } + + /** + * {@inheritDoc} + */ + @Override + public String label() + { + return getScreenValue(false); + } + + /** + * Some widgets can ignore an ENABLE statement (like read-only, hidden, TEXT). + * + * @return true if the ENABLE statement must ignore it. + */ + @Override + public boolean ignoreEnable() + { + return config.explicitViewAs && config.readOnly && config.hidden; + } /** * Copy the widget state from the specified source widget to this instance. === modified file 'src/com/goldencode/p2j/ui/client/TypeAhead.java' --- src/com/goldencode/p2j/ui/client/TypeAhead.java 2019-03-23 19:54:28 +0000 +++ src/com/goldencode/p2j/ui/client/TypeAhead.java 2020-11-25 20:19:41 +0000 @@ -2,7 +2,7 @@ ** Module : TypeAhead.java ** Abstract : Buffered keyboard with CTRL-C processing. ** -** Copyright (c) 2006-2019, Golden Code Development Corporation. +** Copyright (c) 2006-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- --JPRM-- ----------------Description----------------- ** 001 NVS 20060206 @24295 Initial version. @@ -75,6 +75,9 @@ ** key reader. ** 030 HC 20181212 Implemented support for asynchronous key state read emulation. ** 031 CA 20190322 registerMouseWidgets is executed only if we start waiting for events. +** 032 EVL 20201125 Replaced ArrayList with LinkedBlockingDeque as type ahead buffer to accumulate +** key code events. Optimized deque entries retrieving. Also relaxed some sync +** locking for operations with new buffer type. */ /* ** This program is free software: you can redistribute it and/or modify @@ -132,6 +135,7 @@ package com.goldencode.p2j.ui.client; import java.util.*; +import java.util.concurrent.*; import com.goldencode.p2j.security.*; import com.goldencode.p2j.ui.client.driver.*; import com.goldencode.p2j.ui.client.event.*; @@ -148,7 +152,7 @@ private static final ContextLocal local = new ContextLocal<>(); /** The type-ahead buffer which keeps a strict FIFO ordering. */ - private List taBuf = new ArrayList<>(); + private Deque taBuf = new LinkedBlockingDeque<>(512); /** Suspend/resume flag. */ private volatile boolean suspended = false; @@ -310,7 +314,7 @@ /** * Purges the type ahead buffer. */ - public synchronized void clear() + public void clear() { taBuf.clear(); } @@ -569,14 +573,13 @@ * @return The complete event represented by the key code in the type * ahead queue or null if the queue is empty. */ - private synchronized KeyInput dequeueEvent() + private KeyInput dequeueEvent() { KeyInput event = null; if (!taBuf.isEmpty()) { - event = EventManager.eventFromKey(taBuf.get(0)); - taBuf.remove(0); + event = EventManager.eventFromKey(taBuf.poll()); } return event; @@ -680,16 +683,16 @@ continue; } + if (disp == Watcher.CLEAR_AND_QUEUE) + { + taBuf.clear(); + } + + // enqueue the raw key code + taBuf.add(key); + synchronized (TypeAhead.this) { - if (disp == Watcher.CLEAR_AND_QUEUE) - { - taBuf.clear(); - } - - // enqueue the raw key code - taBuf.add(key); - // awaken one waiting thread if there are any that are waiting TypeAhead.this.notify(); } === modified file 'src/com/goldencode/p2j/ui/client/WidgetRegistry.java' --- src/com/goldencode/p2j/ui/client/WidgetRegistry.java 2020-10-04 18:23:10 +0000 +++ src/com/goldencode/p2j/ui/client/WidgetRegistry.java 2021-01-18 17:53:57 +0000 @@ -2,7 +2,7 @@ ** Module : WidgetRegistry.java ** Abstract : ** -** Copyright (c) 2010-2020, Golden Code Development Corporation. +** Copyright (c) 2010-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 SIY 20101015 Widget registry code from Screen moved into separate class. @@ -70,7 +70,18 @@ ** HC 20200928 Fixed widget initialization to prevent editing backup copy. This resolves issues of ** missed config field changes when changes are tracked. ** HC 20201004 Performance optimizations of server-client config state synchronization. +** 042 IAS 20201009 Optimized access to the widgetList, removed getComponents() method +** 043 CA 20201011 Optimized getComponent - if there is no DOWN widget with the same ID, then there is +** no reason to go through the frame to compute it. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. +** GES 20201022 Added min/max ID range calculation which allows screen buffer offset mode to be used. +** EVL 20201106 Optimized getIDs array construction call. Skip one more intremediate array creation and +** minimize memory usage. +** CA 20201112 Avoid pushing the entire frame definition when setting the FRAME or PARENT attribute. +** CA 20210118 Replaced getIDs() with getWidgets() - this is used when building the ScreenBuffer instances, +** and this approach avoids re-calculation of Widget instances from the registry. */ + /* ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU Affero General Public License as @@ -127,7 +138,7 @@ package com.goldencode.p2j.ui.client; import java.util.*; -import java.util.concurrent.locks.*; +import java.util.concurrent.*; import java.util.function.Predicate; import java.util.logging.*; @@ -145,6 +156,7 @@ * * @author SIY * @version 1.0 + * @param OutputManaget type */ public class WidgetRegistry> { @@ -152,14 +164,14 @@ private static final Logger LOG = LogHelper.getLogger(WidgetRegistry.class); /** The list of the widgets in the current screen. */ - private LinkedHashMap> widgetList = new LinkedHashMap<>(); + private ConcurrentMap> widgetList = new ConcurrentHashMap<>(); + + /** The set of registered widgets in a DOWN frame. */ + private Set downWidgetIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** We need to maintain the list of currently editable frames. */ private Map, Set> editableFramesList = new HashMap<>(); - /** Guard lock for access to the widgetList */ - private final ReadWriteLock lock = new ReentrantReadWriteLock(); - /** Flag indicating the UI client is in batch mode. */ private boolean batchMode = false; @@ -174,7 +186,7 @@ * @return The ancestor satisfied the target filter condition or null if there are no * widgets satisfied this condition. */ - public static Widget findAncestor(Widget w, Predicate filter) + public static Widget findAncestor(Widget w, Predicate> filter) { return findAncestor(w, false, filter); } @@ -193,9 +205,9 @@ * @return The ancestor satisfied the target filter condition or null if there are no * widgets satisfied this condition. */ - public static Widget findAncestor(Widget w, boolean system, Predicate filter) + public static Widget findAncestor(Widget w, boolean system, Predicate> filter) { - Widget c = w; + Widget c = w; while (c != null) { if (filter.test(c)) @@ -218,7 +230,7 @@ * @return The descendant having the specified type or null if there are no widgets * satisfied this condition. */ - public static Widget findDescendant(Widget w, Class type) + public static Widget findDescendant(Widget w, Class type) { if (w.getClass().equals(type)) { @@ -227,9 +239,9 @@ else if (w instanceof Container) { int index = 0; - Widget c; - Widget target = null; - for ( ;(c = ((Container) w).get(index)) != null && target == null; index++) + Widget c; + Widget target = null; + for ( ;(c = ((Container) w).get(index)) != null && target == null; index++) { target = findDescendant(c, type); } @@ -263,6 +275,7 @@ public void clear() { widgetList.clear(); + downWidgetIds.clear(); editableFramesList.clear(); } @@ -354,24 +367,24 @@ } } - /** - * Get all components defined in the screen. - * - * @return Array of component. - */ - @SuppressWarnings("unchecked") - public Widget[] getComponents() - { - lock.readLock().lock(); - try - { - return widgetList.values().toArray(new Widget[widgetList.size()]); - } - finally - { - lock.readLock().unlock(); - } - } +// /** +// * Get all components defined in the screen. +// * +// * @return Array of component. +// */ +// @SuppressWarnings("unchecked") +// public Widget[] getComponents() +// { +// lock.readLock().lock(); +// try +// { +// return widgetList.values().toArray(new Widget[widgetList.size()]); +// } +// finally +// { +// lock.readLock().unlock(); +// } +// } /** * Remove the given frame and all contained components from the master @@ -453,7 +466,7 @@ * @param widToCheck * The widget to check. */ - public void addEditableFrameForWidget(Widget widToCheck) + public void addEditableFrameForWidget(Widget widToCheck) { // wirtual widget will not be considered if (WidgetId.virtualWidget(widToCheck.getId())) @@ -487,7 +500,7 @@ * @param widToCheck * The widget to check. */ - public void removeEditableFrameForWidget(Widget widToCheck) + public void removeEditableFrameForWidget(Widget widToCheck) { // wirtual widget will not be considered if (WidgetId.virtualWidget(widToCheck.getId())) @@ -546,22 +559,12 @@ if (id.equals(WidgetId.DEFAULT_WINDOW_ID)) return (Widget) WindowManager.getDefaultWindow(); - Widget w = null; - lock.readLock().lock(); - try - { - w = widgetList.get(id); - } - finally - { - lock.readLock().unlock(); - } - + Widget w = widgetList.get(id); if (w == null) { w = WindowManager.findWindow(id.asInt()); } - + return w; } @@ -588,12 +591,12 @@ { w = getComponent(new WidgetId(id)); } - else + else if (!downWidgetIds.isEmpty() && downWidgetIds.contains(new WidgetDownId(id, 1))) { int frameID = lookupFrameIdFromWidgetId(id); if (frameID != id && frameID != -1) { - Frame frame = (Frame) getComponent(frameID); + Frame frame = (Frame) getComponent(frameID); WidgetId wid = Frame.resolveWidgetId(frame, id); Widget widget = getComponent(wid); @@ -606,6 +609,10 @@ w = getComponent(new WidgetId(id)); } } + else + { + w = getComponent(new WidgetId(id)); + } if (w == null) { @@ -627,11 +634,11 @@ { int resultId = -1; - Widget wid = getComponent(new WidgetId(widgetId)); + Widget wid = getComponent(new WidgetId(widgetId)); if (wid != null) { - Frame widFrame = (Frame) UiUtils.locateFrame(wid); + Frame widFrame = (Frame) UiUtils.locateFrame(wid); if (widFrame != null) { @@ -644,6 +651,24 @@ } /** + * Attach a widget to a frame, at runtime. The widget can be a dynamic widget or a full static frame. + * + * @param frame + * The parent frame. + * @param wid + * The widget ID. + * @param cfg + * The widget's configuration. + */ + public void attachRuntimeWidget(Frame frame, int wid, WidgetConfig cfg) + { + reconstructWidget(cfg); + Widget widget = getComponent(wid); + + attachWidgetToFrame(frame, widget, widget.config()); + } + + /** * Instantiate screen from the given array of definitions. * * @param sd @@ -750,15 +775,15 @@ if (menu != null) { addWidgetsToMenu(menuId, menuDescr); - if (menu instanceof Menu && !((Menu) menu).config().popupOnly) + if (menu instanceof Menu && !((Menu) menu).config().popupOnly) { - ((Menu) menu).setVisible(true); - ((Menu) menu).doLayout(); + ((Menu) menu).setVisible(true); + ((Menu) menu).doLayout(); } else if (menu instanceof SubMenu) { - ((SubMenu) menu).setVisible(true); - ((SubMenu) menu).doLayout(); + ((SubMenu) menu).setVisible(true); + ((SubMenu) menu).doLayout(); } } @@ -768,50 +793,70 @@ } /** - * Get IDs of all widgets in the current screen belonging to the given frame. - * - * @param frameId - * Frame ID whose widget ID info should be returned. - * - * @return Array of all IDs. + * Get all widgets in the current screen belonging to the given frame. + * + * @param frame + * Frame whose widget info should be returned. + * @param range + * A container in which to store the minimum and maximum ID range, if non-null. + * + * @return Array of all widgets. */ - public WidgetId[] getIDs(int frameId) + public Widget[] getWidgets(Frame frame, IdRange range) { - // check if the given frame is in registry - Frame frame = (Frame) getComponent(frameId); - - if (frame == null) - { - return null; - } - // get the list of widgets from frame - Widget[] list = frame.getContentPane().widgets(); + List> list = frame.getContentPane().widgetsAsList(false); - if (list == null || list.length == 0) + if (list == null || list.size() == 0) { return null; } // enumerate all widgets and get ID - List res = new ArrayList<>(); + ArrayList> res = new ArrayList<>(list.size()); - for (int i = 0; i < list.length; i++) + for (Widget w : list) { - res.add(UiUtils.getWidgetId(list[i])); - - if (list[i] instanceof Browse) - { - Browse brws = (Browse) list[i]; - for (int j = 0; j < brws.getColumnCount(); j++) + res.add(w); + + WidgetId wid = w.getId(); + + if (range != null) + { + int widAsInt = wid.asInt(); + range.min = Math.min(range.min, widAsInt); + range.max = Math.max(range.max, widAsInt); + } + + if (w instanceof Browse) + { + Browse brws = (Browse) w; + int columnCount = brws.getColumnCount(); + if (columnCount > 0) { - BrowseColumnConfig bcfg = brws.getColumnCfg(j); - res.add(bcfg.id); + // prepare enough room to store colums ID + res.ensureCapacity(res.size() + columnCount); + for (int j = 0; j < columnCount; j++) + { + BrowseColumnConfig bcfg = brws.getColumnCfg(j); + if (bcfg.id != null) + { + res.add(getComponent(bcfg.id)); + + if (range != null) + { + int col = bcfg.id.asInt(); + + range.min = Math.min(range.min, col); + range.max = Math.max(range.max, col); + } + } + } } } } - - return res.toArray(new WidgetId[res.size()]); + + return res.toArray(new Widget[res.size()]); } /** @@ -835,14 +880,21 @@ */ public void addWidget(WidgetId id, Widget widget) { - lock.writeLock().lock(); - try - { - this.widgetList.put(id, widget); - } - finally - { - lock.writeLock().unlock(); + if (id == null) + { + LOG.warning("An attempt to register widget with null id, widget: " + + (widget == null ? "null" : widget.getClass().getName())); + return; + } + if (widget == null) + { + LOG.warning("An attempt to register null widget with id: " + id.toString()); + return; + } + this.widgetList.put(id, widget); + if (id instanceof WidgetDownId) + { + this.downWidgetIds.add((WidgetDownId) id); } } @@ -851,18 +903,21 @@ * * @param id * The widget ID. + * @return removed widget */ - public Widget removeWidget(WidgetId id) + public Widget removeWidget(WidgetId id) { - lock.writeLock().lock(); - try - { - return this.widgetList.remove(id); - } - finally - { - lock.writeLock().unlock(); - } + if (id == null) + { + LOG.warning("An attempt to remove widget with null id"); + return null; + } + + if (id instanceof WidgetDownId) + { + downWidgetIds.remove(id); + } + return this.widgetList.remove(id); } /** @@ -1028,68 +1083,85 @@ WidgetConfig config = screen.getConfig(widgetIndex); Widget nextWidget = getComponent(widgetIndex); - // associate browse columns with their browse widget which is a - // container (of sorts) itself - if (config instanceof BrowseColumnConfig) - { - Browse browse = (Browse) getComponent(((BrowseColumnConfig) config).browseId); - - if (browse != null) - ((BrowseColumn) nextWidget).setBrowse(browse); + attachWidgetToFrame(frame, nextWidget, config); + } + } - continue; - } - - // determine if the frame's parent has changed. if so, attach it - if (config instanceof FrameConfig && !((Frame) nextWidget).isRootFrame()) - { - Container nparent = nextWidget.parent(); - - Container parent = (Container) UiUtils.locateFrame(nparent); - if (parent == null) - { - // not attached to a frame, get its direct parent - parent = nparent; - } - - if (parent != frame) - { - if (parent != null) + /** + * Attach a widget to a frame, at runtime. The widget can be a dynamic widget or a full static frame. + * + * @param frame + * The parent frame. + * @param widget + * The widget. + * @param cfg + * The widget's configuration. + */ + private void attachWidgetToFrame(Frame frame, Widget widget, WidgetConfig config) + { + // associate browse columns with their browse widget which is a + // container (of sorts) itself + if (config instanceof BrowseColumnConfig) + { + Browse browse = (Browse) getComponent(((BrowseColumnConfig) config).browseId); + + if (browse != null) + { + ((BrowseColumn) widget).setBrowse(browse); + } + + return; + } + + // determine if the frame's parent has changed. if so, attach it + if (config instanceof FrameConfig && !((Frame) widget).isRootFrame()) + { + Container nparent = widget.parent(); + + Container parent = (Container) UiUtils.locateFrame(nparent); + if (parent == null) + { + // not attached to a frame, get its direct parent + parent = nparent; + } + + if (parent != frame) + { + if (parent != null) + { + nparent.detach(widget); + + if (widget.isVisible()) { - nparent.detach(nextWidget); - - if (nextWidget.isVisible()) - { - parent.repaint(); - } + parent.repaint(); } - - frame.addWidget(nextWidget); } + + frame.addWidget(widget); } - - // check if this widget has already had the frame assigned as the - // parent, if so then we MUST NOT add it to the widget list again - // since it is already there (the Container._addComponent() method - // sets the parent reference after unconditionally adding the - // component to the components vector - if (nextWidget.parent() != null) - continue; - - // make widget disabled by default unless it is dynamic - if (nextWidget.isEnabled()) + } + + // check if this widget has already had the frame assigned as the + // parent, if so then we MUST NOT add it to the widget list again + // since it is already there (the Container._addComponent() method + // sets the parent reference after unconditionally adding the + // component to the components vector + if (widget.parent() != null) + { + return; + } + + // make widget disabled by default unless it is dynamic + if (widget.isEnabled()) + { + WidgetConfig cfg = widget.config(); + if (!(cfg != null && cfg instanceof BaseConfig && ((BaseConfig) cfg).dynamic)) { - WidgetConfig cfg = nextWidget.config(); - if (!(cfg != null && - cfg instanceof BaseConfig && - ((BaseConfig) cfg).dynamic)) - { - nextWidget.setEnabled(false); - } + widget.setEnabled(false); } - - frame.addWidget(nextWidget); } + + frame.addWidget(widget); } /** @@ -1127,13 +1199,13 @@ /** * Reconstruct the side-label with the specified config. *

- * This will not create a new {@link Label} instance. It will only register the widget + * This will not create a new {@link ClientDrivenLabel} instance. It will only register the widget * configuration with the {@link ConfigManager}, so that changes to the label instance can be * tracked and sent back to the server. *

* Note that the registered widget configuration must be "linked" to the related label widget. * This is because the widget takes care of unregistering the label configuration. The link - * is established in {@link Label#linkTo(int, int)}. + * is established in {@link ClientDrivenLabel#linkTo(int, int)}. * * @param config * Widget snapshot taken from the server. @@ -1155,26 +1227,26 @@ { // this may be a side-label in a down body - determine the widget to which it belongs, // and from there the row - Label lbl = (Label) getComponent(cid); - if (lbl != null) + Label lbl = (Label) getComponent(cid); + if (lbl instanceof ClientDrivenLabel) { - Widget sideWidget = lbl.getSideWidget(); + Widget sideWidget = ((ClientDrivenLabel) lbl).getSideWidget(); if (sideWidget != null && sideWidget instanceof LabeledWidget) { // determine the frame - Frame frame = (Frame) UiUtils.locateFrame(sideWidget); + Frame frame = (Frame) UiUtils.locateFrame(sideWidget); if (frame != null && frame.isSideLabels()) { - sideWidget = frame.getField((Widget) sideWidget); + sideWidget = frame.getField((Widget) sideWidget); if (sideWidget instanceof LabeledWidget) { - lbl = ((LabeledWidget) sideWidget).getLabelInstance(); + lbl = (ClientDrivenLabel) ((LabeledWidget) sideWidget).getLabelInstance(); cid = lbl.getId(); } } } } - else + else if (lbl == null) { LOG.log(Level.SEVERE, "reconstrctSideLabel missing sideWidget " + cid); } @@ -1191,4 +1263,16 @@ // restore the id config.id = saveID; } + + /** + * Container class to store the minimum and maximum widget IDs. + */ + public static class IdRange + { + /** Smallest widget ID in the range. */ + public int min = -1; + + /** Largest widget ID in the range. */ + public int max = -1; + } } === modified file 'src/com/goldencode/p2j/ui/client/Window.java' --- src/com/goldencode/p2j/ui/client/Window.java 2020-04-20 21:50:49 +0000 +++ src/com/goldencode/p2j/ui/client/Window.java 2020-11-20 18:40:59 +0000 @@ -275,7 +275,8 @@ ** widgets. ** 141 HC 20200313 Javadoc fixes. ** RFB 20200420 Added support for KEEP-MESSAGES option when output is redirected. -** Missed the suppression of the pauseBeforeHide when keepMessages is on. +** Missed the suppression of the pauseBeforeHide when keepMessages is on. +** 142 CA 20201015 Replaced java.util.Stack with ArrayDeque (as synchronization is not required). */ /* ** This program is free software: you can redistribute it and/or modify @@ -2658,6 +2659,6 @@ private static final class WorkArea { /** Statuses data stack. Entry contains statusTypes and input counter */ - private final Stack statusStack = new Stack(); + private final Deque statusStack = new ArrayDeque(); } } === modified file 'src/com/goldencode/p2j/ui/client/WindowManager.java' --- src/com/goldencode/p2j/ui/client/WindowManager.java 2020-03-16 17:30:47 +0000 +++ src/com/goldencode/p2j/ui/client/WindowManager.java 2020-11-04 17:08:46 +0000 @@ -116,6 +116,9 @@ ** 063 HC 20191023 Added support for WM_SIZE, WM_MOVE and WM_EXITSIZEMOVE messages delivered by ** Message blaster. ** 064 HC 20200313 Javadoc fixes. +** 065 CA 20201011 Optimized findWindow(int) - keep a map of windows by their ID, for fast access. +** CA 20201015 Optimized isDisplayed(int) - keep a fast-access bitmap of window IDs. +** EVL 20201104 Fixed regression in ChUI for drop-down widget without ID. */ /* ** This program is free software: you can redistribute it and/or modify @@ -177,6 +180,8 @@ import java.util.logging.*; import java.util.stream.*; +import org.roaringbitmap.RoaringBitmap; + import com.goldencode.p2j.security.*; import com.goldencode.p2j.ui.*; import com.goldencode.p2j.ui.chui.ThinClient; @@ -341,6 +346,14 @@ { wa.windows.add(window); wa.fixedOrderWindows.add(window); + // consider ChUI specific here (drop down has no wisget ID) + WidgetId wid = window.getId(); + if (wid != null) + { + int widInt = wid.asInt(); + wa.windowIDs.put(widInt, window); + wa.windowIDSet.add(widInt); + } } } } @@ -628,10 +641,7 @@ @SuppressWarnings({ "rawtypes", "unchecked" }) public static T findWindow(int windowId) { - return (T) windowList().stream() - .filter((TitledWindow w) -> WidgetId.equals(w.getId(), windowId)) - .findFirst() - .orElse(null); + return (T) work.get().windowIDs.get(windowId); } /** @@ -666,6 +676,8 @@ synchronized (wa.windows) { wa.windows.clear(); + wa.windowIDs.clear(); + wa.windowIDSet.clear(); wa.fixedOrderWindows.clear(); } } @@ -680,7 +692,7 @@ { // ChUI has a peculiarity here... it requires the top window (if is a message-box or // something else) regardless of visibility state. - boolean isChui = ThinClient.getInstance().isChui(); + boolean isChui = work.get().isChUI; return Utils.reverseStream(windowList()) .filter((TitledWindow w) -> isChui || (w.isVisible() && !(w instanceof DropDown))) @@ -1058,7 +1070,7 @@ // activate the window only after it has been added to window manager om.registerWindow(window); - if (ThinClient.getInstance().isChui()) + if (work.get().isChUI) { // in ChUI the default window is always the active window window.setVisible(true); @@ -1133,7 +1145,16 @@ synchronized (wa.windows) { - return wa.windows.contains(window); + // ChUI has specific for drop-down, no widget ID + WidgetId wid = window.getId(); + if (wa.isChUI || wid == null) + { + return wa.windows.contains(window); + } + else + { + return wa.windowIDSet.contains(wid.asInt()); + } } } @@ -1179,6 +1200,14 @@ wa.windows.remove(window); wa.fixedOrderWindows.remove(window); wa.currentWindowsStack.remove(window); + // in ChUI the widget ID is null for drop-down + WidgetId wid = window.getId(); + if (wid != null) + { + int widInt = wid.asInt(); + wa.windowIDs.remove(widInt); + wa.windowIDSet.remove(widInt); + } Map> delayed = wa.delayedMinimizeWindows; delayed.remove(window); @@ -2300,9 +2329,18 @@ * Stores global data relating to the state of the current context. */ private static class WorkArea - { + { + /** Check if the current session GUI or ChUI. */ + private boolean isChUI = ThinClient.getInstance().isChui(); + + /** A map to identify each window by its ID. */ + private Map> windowIDs = new HashMap<>(); + /** Storage for windows. */ private List> windows = new ArrayList<>(); + + /** A bitmap for all window IDs. */ + private RoaringBitmap windowIDSet = new RoaringBitmap(); /** Storage for windows, following their creation order. */ private List> fixedOrderWindows = new ArrayList<>(); === modified file 'src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java' --- src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/client/ZeroColumnLayout.java 2020-10-24 22:32:57 +0000 @@ -313,6 +313,7 @@ ** HC 20180611 Fixed calculation of frame header when the frame has no labels ** defined. ** 142 SBI 20200820 Fixed field placements with no-labels. +** HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* ** This program is free software: you can redistribute it and/or modify @@ -666,7 +667,7 @@ // determine presence of headers and drawing mode for (int i = 0; i < c.length; i++) { - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) continue; if (UiUtils.hasConfig(c[i])) @@ -814,7 +815,7 @@ if (hasHeaders && i <= (lastHeader + 1)) continue; - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) continue; if (c[i] instanceof Skip) @@ -940,16 +941,16 @@ // this is used to draw the horizontal line below headers, it is a // bogus widget with no text but with underlining - Label delimiter = null; + ClientDrivenLabel delimiter = null; // first pass, calculate size for (int i = 0; i < c.length; i++) { - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) { - if (((Label) c[i]).isDelimiter()) + if (((ClientDrivenLabel) c[i]).isDelimiter()) { - delimiter = (Label) c[i]; + delimiter = (ClientDrivenLabel) c[i]; } continue; @@ -1144,7 +1145,7 @@ // second pass, calculate/relocate for (int i = 0; i < c.length; i++) { - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) { continue; } @@ -1553,7 +1554,7 @@ { vars.startColumn = 0; - if (c[i] instanceof Label && ((Label) c[i]).isDelimiter()) + if (c[i] instanceof ClientDrivenLabel && ((ClientDrivenLabel) c[i]).isDelimiter()) { container.remove(c[i]); continue; @@ -1566,7 +1567,7 @@ vars.hdrWidth = vars.nextColumn; } - if (c[i] instanceof Label) + if (c[i] instanceof ClientDrivenLabel) continue; vars.initWidgetLocation(i); @@ -1919,7 +1920,7 @@ // safe, comparison of scaled doubles if (labelText != null && (labels || fin.isForceLabel()) && !fin.isNoLabels()) { - Label label = null; + ClientDrivenLabel label = null; Dimension sz = c[i].dimension(); WidgetId nextId = c[i].getId().createId(WidgetId.nextID()); @@ -2295,8 +2296,8 @@ Dimension sz = c[i].dimension(); String labelText = getLabel(fin); Label removed = fin.getLabelInstance(); - - Label label = null; + + ClientDrivenLabel label = null; WidgetId nextId = c[i].getId().createId(WidgetId.nextID()); // side-label space width including colon and space @@ -2930,7 +2931,7 @@ { Label label = ((LabeledWidget) w).getLabelInstance(); if (label != null && - ConfigHelper.getAssignedColumn(label.config) == BaseConfig.INV_COORD) + ConfigHelper.getAssignedColumn(label.config()) == BaseConfig.INV_COORD) { String labelText = getLabel((LabeledWidget) w); double labelSpaceWith = labelWidth((LabeledWidget) w, labelText + ": "); @@ -2952,7 +2953,7 @@ private void setLabelLocation(Label label, double col, double row) { label.setLocation(col, row); - label.config.widgetPlaced = true; + label.config().widgetPlaced = true; } } === modified file 'src/com/goldencode/p2j/ui/client/chui/ChuiLinuxKeyboard.java' --- src/com/goldencode/p2j/ui/client/chui/ChuiLinuxKeyboard.java 2017-04-01 23:33:34 +0000 +++ src/com/goldencode/p2j/ui/client/chui/ChuiLinuxKeyboard.java 2020-10-11 16:07:42 +0000 @@ -2,10 +2,11 @@ ** Module : ChuiLinuxKeyboard.java ** Abstract : Keyboard configuration for Linux ChUI clients. ** -** Copyright (c) 2014-2017, Golden Code Development Corporation. +** Copyright (c) 2014-2020, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 CA 20141029 Created initial version. +** 002 CA 20201011 Improved findKey and findKeyEnd performance. */ /* ** This program is free software: you can redistribute it and/or modify @@ -96,7 +97,9 @@ basicKeys[618] = "MOUSE-MOVE-DOWN"; basicKeys[626] = "MOUSE-MOVE-CLICK"; basicKeys[650] = "MOUSE-MOVE-DBLCLICK"; - + + initBasicKeyCodes(); + // filling the map of alternate key labels addAltLabel("ENTER", 13); addAltLabel("SHIFT-TAB", 509); === modified file 'src/com/goldencode/p2j/ui/client/chui/ChuiWindowsKeyboard.java' --- src/com/goldencode/p2j/ui/client/chui/ChuiWindowsKeyboard.java 2017-04-01 23:33:34 +0000 +++ src/com/goldencode/p2j/ui/client/chui/ChuiWindowsKeyboard.java 2021-01-28 20:00:25 +0000 @@ -2,10 +2,12 @@ ** Module : ChuiWindowsKeyboard.java ** Abstract : Keyboard configuration for Windows ChUI clients. ** -** Copyright (c) 2014-2017, Golden Code Development Corporation. +** Copyright (c) 2014-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 CA 20141029 Created initial version. +** 002 CA 20201011 Improved findKey and findKeyEnd performance. +** 003 CA 20210127 Fixed key-code to detect extended labels. */ /* ** This program is free software: you can redistribute it and/or modify @@ -86,17 +88,6 @@ @Override public void init() { - super.init(); - - basicKeys[13] = "ENTER"; - basicKeys[509] = "SHIFT-TAB"; - basicKeys[512] = "DELETE"; - basicKeys[525] = "ENTER"; - basicKeys[610] = "MIDDLE-MOUSE-UP"; - basicKeys[618] = "MIDDLE-MOUSE-DOWN"; - basicKeys[626] = "MIDDLE-MOUSE-CLICK"; - basicKeys[650] = "MIDDLE-MOUSE-DBLCLICK"; - // differences for multi-byte keys extendedKeys.put(1021, "SHIFT-SHIFT-TAB"); extendedKeys.put(1024, "ALT"); @@ -118,6 +109,19 @@ extendedKeys.put(3597, "CTRL-SHIFT-ALT-ENTER"); extendedKeys.put(4093, "CTRL-SHIFT-ALT-SHIFT-TAB"); + super.init(); + + basicKeys[13] = "ENTER"; + basicKeys[509] = "SHIFT-TAB"; + basicKeys[512] = "DELETE"; + basicKeys[525] = "ENTER"; + basicKeys[610] = "MIDDLE-MOUSE-UP"; + basicKeys[618] = "MIDDLE-MOUSE-DOWN"; + basicKeys[626] = "MIDDLE-MOUSE-CLICK"; + basicKeys[650] = "MIDDLE-MOUSE-DBLCLICK"; + + initBasicKeyCodes(); + // filling the map of alternate key labels addAltLabel("RETURN", 13); addAltLabel("BACK-TAB", 509); @@ -272,4 +276,4 @@ { return code == 634 || super.useRule3(code); } -} \ No newline at end of file +} === modified file 'src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebPageHandler.java' --- src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebPageHandler.java 2018-04-05 06:32:08 +0000 +++ src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebPageHandler.java 2021-01-14 08:53:32 +0000 @@ -2,7 +2,7 @@ ** Module : ChuiWebPageHandler.java ** Abstract : ChUI-specific page handler ** -** Copyright (c) 2013-2017 Golden Code Development Corporation. +** Copyright (c) 2013-2021 Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 MAG 20131105 Created initial version. @@ -16,6 +16,7 @@ ** 008 GES 20150717 Moved clipboard processing back here because it is not conditional for GUI. ** 009 SBI 20160601 Added graphicsCached parameter to the index.html template. ** 010 SBI 20180405 Changed to use template key and value provider. +** 011 HC 20210114 Added missing template parameters. */ /* ** This program is free software: you can redistribute it and/or modify @@ -204,6 +205,10 @@ { return "false"; }); + + add("disablePixelManipulation", () -> "false"); + + add("renderer", () -> "canvas2d"); } } } \ No newline at end of file === modified file 'src/com/goldencode/p2j/ui/client/driver/WidgetFactory.java' --- src/com/goldencode/p2j/ui/client/driver/WidgetFactory.java 2020-03-16 17:30:47 +0000 +++ src/com/goldencode/p2j/ui/client/driver/WidgetFactory.java 2020-10-24 22:32:57 +0000 @@ -32,6 +32,7 @@ ** 017 OM 20170815 Added createFileSystemChooserDialog() method. ** 019 HC 20200130 Added support for dynamic registration of widget implementations. ** 020 HC 20200313 Javadoc fixes. +** 021 HC 20201024 Implemented SIDE-LABEL-HANDLE attribute. */ /* @@ -112,7 +113,7 @@ MnemonicInfo createMnemonic(); /** - * Create new {@link Label} instance for given text. + * Create new {@link ClientDrivenLabel} instance for given text. * * @param id * The widget's ID. @@ -122,9 +123,9 @@ * The ID of the frame to which the label belongs, or -1 if unknown at the time of the * creation. * - * @return new {@link Label} instance. + * @return new {@link ClientDrivenLabel} instance. */ - Label createLabel(WidgetId id, String text, int frameId); + ClientDrivenLabel createLabel(WidgetId id, String text, int frameId); /** * Create new button for given container and text. @@ -235,19 +236,19 @@ ScrollPane createScrollPane(ScrollableWidget widget, Supplier> wnd); /** - * Create UI-specific {@link Label} instance as a copy of given {@link Label}. + * Create UI-specific {@link ClientDrivenLabel} instance as a copy of given {@link ClientDrivenLabel}. * * @param id * The widget's ID. * @param labelInstance - * Source {@link Label} instance. + * Source {@link ClientDrivenLabel} instance. * @param frameId * The ID of the frame to which the label belongs, or -1 if unknown at the time of the * creation. * - * @return instance of UI-specific implementation of {@link Label}. + * @return instance of UI-specific implementation of {@link ClientDrivenLabel}. */ - Label createLabel(WidgetId id, Label labelInstance, int frameId); + ClientDrivenLabel createLabel(WidgetId id, ClientDrivenLabel labelInstance, int frameId); /** * Create UI-specific {@link RadioButton} instance. === modified file 'src/com/goldencode/p2j/ui/client/driver/swing/WinKeyboardReader.java' --- src/com/goldencode/p2j/ui/client/driver/swing/WinKeyboardReader.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/client/driver/swing/WinKeyboardReader.java 2021-01-28 17:31:08 +0000 @@ -3,7 +3,7 @@ ** Abstract : process swing key events and convert them to 4GL keys, compatible with Windows ** clients. ** -** Copyright (c) 2014-2020, Golden Code Development Corporation. +** Copyright (c) 2014-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ---------------------------------Description---------------------------------- ** 001 CA 20141029 Created initial version. @@ -40,6 +40,7 @@ ** AIL 20200309 Restored the key release event generation. ** AIL 20200525 Changed KeyCode called constructor. ** AIL 20200803 Avoid generating key released events for SPACE keys with modifiers. +** CA 20210128 A pure ALT key must be sent to the client (menubar processing). */ /* ** This program is free software: you can redistribute it and/or modify @@ -411,7 +412,7 @@ else { Integer mappedCode; - if (isModifier(lastKeyCode)) + if (isModifier(lastKeyCode) && lastKeyCode != KeyEvent.VK_ALT) { mappedCode = -1; } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2020-09-07 16:23:31 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2021-01-24 20:39:20 +0000 @@ -2,7 +2,7 @@ ** Module : WebClientMessageTypes.java ** Abstract : defines the message header type code for all possible web client protocol messages ** -** Copyright (c) 2014-2020, Golden Code Development Corporation. +** Copyright (c) 2014-2021, Golden Code Development Corporation. ** ** -#- -I- --Date-- ------------------------------Description---------------------------------- ** 001 GES 20150317 Copied from ChuiWebSimulator for shared usage. @@ -56,6 +56,7 @@ ** 026 HC 20200408 Implemented JS client - Java client message throttling, multiple messages ** sent in a group as a single message. ** 027 HC 20200726 Initial implementation of SPREADSHEET widget and related changes. +** HC 20210124 Added messages for enabling or disabling graphics caching. */ /* @@ -414,9 +415,6 @@ /** Ask clipboard contents to be pasted in web client. */ public static final byte MSG_CLIPBOARD_ASK_FOR_PASTE = (byte) 0xB8; - /** Message for triggering high-level driver widget events. */ - public static final byte MSG_TRIGGER_EVENT = (byte) 0xB9; - /** Getting the client's hostname. */ public static final byte MSG_GET_HOST_NAME = (byte) 0xB9; @@ -429,6 +427,15 @@ /** File choose dialog for upload or file selection */ public static final byte MSG_FILE_CHOOSE = (byte) 0xBC; + /** Message for triggering high-level driver widget events. */ + public static final byte MSG_TRIGGER_EVENT = (byte) 0xBD; + + /** Enables graphics caching */ + public static final byte MSG_ENABLE_GRAPHICS_CACHE = (byte) 0xBE; + + /** Disables graphics caching */ + public static final byte MSG_DISABLE_GRAPHICS_CACHE = (byte) 0xBF; + /** Requests the client if the target font is installed. */ public static final byte MSG_IS_FONT_INSTALLED = (byte) 0xF1; === modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java 2020-04-08 22:25:30 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java 2020-11-30 18:01:47 +0000 @@ -25,6 +25,9 @@ ** 011 HC 20191120 Extended SYSTEM-DIALOG GET-FILE with AT-WEB-BROWSER option. ** 012 HC 20200408 Implemented JS client - Java client message throttling, multiple messages ** sent in a group as a single message. +** 013 EVL 20201026 Converting if ... else if into usage of the switch ... case to enable using jump table on +** the JIT compiler optimization. +** EVL 20201130 Small optimizations for text message reading/writing. */ /* ** This program is free software: you can redistribute it and/or modify @@ -397,84 +400,104 @@ { boolean handled = false; - if (length == 1 && message[offset] == MSG_PAGE_LOADED) - { - receiveResult(MSG_PAGE_LOADED, new Object()); - } - else if (length == 25 && message[offset] == MSG_GET_WEB_BASIC_PALETTE) - { - int msgId = readMessageInt32(message, offset + 1); - - int i = 0; - - int c1 = readMessageInt32(message, offset + 5); - int c2 = readMessageInt32(message, offset + 9); - int c3 = readMessageInt32(message, offset + 13); - int c4 = readMessageInt32(message, offset + 17); - int c5 = readMessageInt32(message, offset + 21); - - receiveResult(msgId, new int[] {c1, c2, c3, c4, c5}); - handled = true; - } - else if (length == 4 && message[offset] == MSG_KEY) - { - int keystroke = readMessageInt16(message, offset + 1); - byte keystate = message[offset + 3]; - - synchronized (lock) - { - keyBuffer.offerLast(new KeyCode(keystroke, keystate)); - lock.notifyAll(); - } - - handled = true; - } - else if (length == 5 && message[offset] == MSG_KEY_VT100) - { - int keyCode = readMessageInt16(message, offset + 1); - int charCode = readMessageInt16(message, offset + 3); - - callbacks.injectVT100Key(keyCode, (char) charCode); - - handled = true; - } - else if (length > 1 && message[offset] == MSG_PASTE) - { - paste(message, offset, length); - handled = true; - } - else if (length > 11 && message[offset] == MSG_PARTIAL) - { - getMessagesCollector().processPartialMesssage(message, offset); - handled = true; - } - else if (message[offset] == MSG_PING_PONG && length == 1) - { - sendBinaryMessage(MSG_PING_PONG); - handled = true; - } - else if (message[offset] == MSG_QUIT && length == 1) - { - // exit the client session - System.exit(0); - } - else if (message[offset] == MSG_MULTIPLEX) - { - offset++; - - int msgCount = readMessageInt32(message, offset); - offset += 4; - - for (int i = 0; i < msgCount; i++) - { - int len = readMessageInt32(message, offset); + switch (message[offset]) + { + case MSG_PAGE_LOADED: + if (length == 1) + { + receiveResult(MSG_PAGE_LOADED, new Object()); + } + break; + case MSG_GET_WEB_BASIC_PALETTE: + if (length == 25) + { + int msgId = readMessageInt32(message, offset + 1); + + int i = 0; + + int c1 = readMessageInt32(message, offset + 5); + int c2 = readMessageInt32(message, offset + 9); + int c3 = readMessageInt32(message, offset + 13); + int c4 = readMessageInt32(message, offset + 17); + int c5 = readMessageInt32(message, offset + 21); + + receiveResult(msgId, new int[] {c1, c2, c3, c4, c5}); + handled = true; + } + break; + case MSG_KEY: + if (length == 4) + { + int keystroke = readMessageInt16(message, offset + 1); + byte keystate = message[offset + 3]; + + synchronized (lock) + { + keyBuffer.offerLast(new KeyCode(keystroke, keystate)); + lock.notifyAll(); + } + + handled = true; + } + break; + case MSG_KEY_VT100: + if (length == 5) + { + int keyCode = readMessageInt16(message, offset + 1); + int charCode = readMessageInt16(message, offset + 3); + + callbacks.injectVT100Key(keyCode, (char) charCode); + + handled = true; + } + break; + case MSG_PASTE: + if (length > 1) + { + paste(message, offset, length); + handled = true; + } + break; + case MSG_PARTIAL: + if (length > 11) + { + getMessagesCollector().processPartialMesssage(message, offset); + handled = true; + } + break; + case MSG_PING_PONG: + if (length == 1) + { + sendBinaryMessage(MSG_PING_PONG); + handled = true; + } + break; + case MSG_QUIT: + if (length == 1) + { + // exit the client session + System.exit(0); + } + break; + case MSG_MULTIPLEX: + offset++; + + int msgCount = readMessageInt32(message, offset); offset += 4; - processBinaryMessage(message, offset, len); - offset += len; - } - - handled = true; + for (int i = 0; i < msgCount; i++) + { + int len = readMessageInt32(message, offset); + offset += 4; + + processBinaryMessage(message, offset, len); + offset += len; + } + + handled = true; + break; + default: + // nothing to do here } return handled; @@ -711,14 +734,20 @@ */ public String readMessageText(byte[] message, int startPos, int endPos) { - StringBuilder sb = new StringBuilder(); - - for (int i = startPos; i < endPos; i += 2) - { - sb.append((char) readMessageInt16(message, i)); - } - - return sb.toString(); + if (endPos > startPos + 1) + { + char[] chars = new char[(endPos - startPos)/2]; + for (int i = startPos, j = 0; i < endPos; i += 2, j++) + { + chars[j] = (char) readMessageInt16(message, i); + } + + return new String(chars); + } + else + { + return ""; + } } /** @@ -806,7 +835,15 @@ for (int i = 0; i < len; i++) { // TODO: all unicode chars won't necessarily fit in 16-bits - offset = writeMessageInt16(message, offset, (int) text.charAt(i)); + if (i + 1 < len) + { + offset = writeMessageInt32(message, offset, + (((int) text.charAt(i)) << 16) | (int) text.charAt(++i)); + } + else + { + offset = writeMessageInt16(message, offset, (int) text.charAt(i)); + } } return offset; } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/index.html' --- src/com/goldencode/p2j/ui/client/driver/web/index.html 2020-06-13 18:20:42 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/index.html 2020-12-02 15:02:19 +0000 @@ -57,6 +57,8 @@ + +