Commit b3e6d445cbf73da2b00062c3f639c2453041ee41

Authored by Jay Berkenbilt
1 parent 3661f274

Tweak "AndGet" mutator functions again

Remove any ambiguity around whether old or new value is being
returned.
ChangeLog
1 1 2022-07-24 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * QPDFObjectHandle: for the methods insertItem, appendItem,
  4 + eraseItem, replaceKey, and removeKey, add a corresponding
  5 + "AndGetNew" and/or "AndGetOld" methods. The ones that end with
  6 + "AndGetNew" return the newly added item. The ones that end with
  7 + "AndGetOld" return the old value. The AndGetNew methods make it
  8 + possible to create a new object, add it to an array or dictionary,
  9 + and get a handle to it all in one line. The AndGetOld methods make
  10 + it easier to retrieve an old value when removing or replacing it.
  11 +
3 12 * Thanks to m-holger for doing significant cleanup of private APIs
4 13 and internals around QPDFObjGen and for significantly improving
5 14 the performance of QPDFObjGen -- See #731. This includes a few
... ... @@ -168,16 +177,6 @@
168 177 128-bit without AES) an error rather than a warning when
169 178 --allow-weak-crypto is not specified. Fixes #576.
170 179  
171   -2022-04-29 Jay Berkenbilt <ejb@ql.org>
172   -
173   - * QPDFObjectHandle: for the methods insertItem, appendItem,
174   - eraseItem, replaceKey, and removeKey, add a corresponding "AndGet"
175   - method (insertItemAndGet, appendItemAndGet, eraseItemAndGet,
176   - replaceKeyAndGet, and removeKeyAndGet) that returns the newly
177   - inserted, replaced, or removed item. This makes it possible to
178   - create a new object, add it to an array or dictionary, and get a
179   - handle to it all in one line.
180   -
181 180 2022-04-24 Jay Berkenbilt <ejb@ql.org>
182 181  
183 182 * Bug fix: "removeAttachment" in the job JSON now takes an array
... ...
... ... @@ -8,29 +8,6 @@ Before Release:
8 8 * Stay on top of https://github.com/pikepdf/pikepdf/pull/315
9 9 * Release qtest with updates to qtest-driver and copy back into qpdf
10 10  
11   -Parent pointer idea:
12   -
13   -* Have replaceKey, removeKey, and eraseItem return the old values. The
14   - comments will clarify the difference between these and the andGet
15   - versions.
16   -* Add std::weak_ptr<QPDFObject> parent to QPDFObject. When adding a
17   - direct object to an array or dictionary, set its parent. When
18   - removing it, clear the parent pointer.
19   -* When a direct object that already has a parent is added to
20   - something, it is a warning and will become an error in qpdf 12.
21   - There needs to be unsafe add methods used by unsafeShallowCopy.
22   - These will add but not modify the parent pointer.
23   -
24   -This allows an object to be moved from one object to another by
25   -removing it, which returns the now orphaned object, and then inserting
26   -it somewhere else. It also doesn't break the pattern of adding a
27   -direct object to something and subsequently mutating it. It just
28   -prevents the same object from being added to more than one thing.
29   -
30   -Note that arrays and dictionaries still need to contain
31   -QPDFObjectHandle because of indirect objects. This only pertains to
32   -direct objects, which are always "resolved" in QPDFObjectHandle.
33   -
34 11 Next:
35 12 * JSON v2 fixes
36 13  
... ... @@ -70,6 +47,26 @@ Pending changes:
70 47 about the case of more than 65,536 pages. If the top node has more
71 48 than 256 children, we'll live with it.
72 49  
  50 +Parent pointer idea:
  51 +
  52 +* Add std::weak_ptr<QPDFObject> parent to QPDFObject. When adding a
  53 + direct object to an array or dictionary, set its parent. When
  54 + removing it, clear the parent pointer.
  55 +* When a direct object that already has a parent is added to
  56 + something, it is a warning and will become an error in qpdf 12.
  57 + There needs to be unsafe add methods used by unsafeShallowCopy.
  58 + These will add but not modify the parent pointer.
  59 +
  60 +This allows an object to be moved from one object to another by
  61 +removing it, which returns the now orphaned object, and then inserting
  62 +it somewhere else. It also doesn't break the pattern of adding a
  63 +direct object to something and subsequently mutating it. It just
  64 +prevents the same object from being added to more than one thing.
  65 +
  66 +Note that arrays and dictionaries still need to contain
  67 +QPDFObjectHandle because of indirect objects. This only pertains to
  68 +direct objects, which are always "resolved" in QPDFObjectHandle.
  69 +
73 70 Soon: Break ground on "Document-level work"
74 71  
75 72  
... ...
include/qpdf/QPDFObjectHandle.hh
... ... @@ -999,18 +999,15 @@ class QPDFObjectHandle
999 999  
1000 1000 // Mutator methods.
1001 1001  
1002   - // Since qpdf 11: when a mutator object returns QPDFObjectHandle&,
1003   - // it is a reference to the object itself. This makes it possible
1004   - // to use a fluent style. For example:
  1002 + // Since qpdf 11: for mutators that may add or remove an item,
  1003 + // there are additional versions whose names contain "AndGet" that
  1004 + // return the added or removed item. For example:
1005 1005 //
1006   - // array.appendItem(i1).appendItem(i2);
1007   - //
1008   - // would append i1 and then i2 to the array. There are also items
1009   - // that end with AndGet and return a QPDFObjectHandle. These
1010   - // return the newly added object. For example:
1011   - //
1012   - // auto new_dict = dict.replaceKeyAndGet(
  1006 + // auto new_dict = dict.replaceKeyAndGetNew(
1013 1007 // "/New", QPDFObjectHandle::newDictionary());
  1008 + //
  1009 + // auto old_value = dict.replaceKeyAndGetOld(
  1010 + // "/New", "(something)"_qpdf);
1014 1011  
1015 1012 // Recursively copy this object, making it direct. An exception is
1016 1013 // thrown if a loop is detected. With allow_streams true, keep
... ... @@ -1036,20 +1033,20 @@ class QPDFObjectHandle
1036 1033 void insertItem(int at, QPDFObjectHandle const& item);
1037 1034 // Like insertItem but return the item that was inserted.
1038 1035 QPDF_DLL
1039   - QPDFObjectHandle insertItemAndGet(int at, QPDFObjectHandle const& item);
  1036 + QPDFObjectHandle insertItemAndGetNew(int at, QPDFObjectHandle const& item);
1040 1037 // Append an item to an array.
1041 1038 QPDF_DLL
1042 1039 void appendItem(QPDFObjectHandle const& item);
1043 1040 // Append an item, and return the newly added item.
1044 1041 QPDF_DLL
1045   - QPDFObjectHandle appendItemAndGet(QPDFObjectHandle const& item);
  1042 + QPDFObjectHandle appendItemAndGetNew(QPDFObjectHandle const& item);
1046 1043 // Remove the item at that position, reducing the size of the
1047 1044 // array by one.
1048 1045 QPDF_DLL
1049 1046 void eraseItem(int at);
1050 1047 // Erase and item and return the item that was removed.
1051 1048 QPDF_DLL
1052   - QPDFObjectHandle eraseItemAndGet(int at);
  1049 + QPDFObjectHandle eraseItemAndGetOld(int at);
1053 1050  
1054 1051 // Mutator methods for dictionary objects
1055 1052  
... ... @@ -1060,14 +1057,19 @@ class QPDFObjectHandle
1060 1057 // Replace value of key and return the value.
1061 1058 QPDF_DLL
1062 1059 QPDFObjectHandle
1063   - replaceKeyAndGet(std::string const& key, QPDFObjectHandle const& value);
  1060 + replaceKeyAndGetNew(std::string const& key, QPDFObjectHandle const& value);
  1061 + // Replace value of key and return the old value, or null if the
  1062 + // key was previously not present.
  1063 + QPDF_DLL
  1064 + QPDFObjectHandle
  1065 + replaceKeyAndGetOld(std::string const& key, QPDFObjectHandle const& value);
1064 1066 // Remove key, doing nothing if key does not exist.
1065 1067 QPDF_DLL
1066 1068 void removeKey(std::string const& key);
1067 1069 // Remove key and return the old value. If the old value didn't
1068 1070 // exist, return a null object.
1069 1071 QPDF_DLL
1070   - QPDFObjectHandle removeKeyAndGet(std::string const& key);
  1072 + QPDFObjectHandle removeKeyAndGetOld(std::string const& key);
1071 1073  
1072 1074 // ABI: Remove in qpdf 12
1073 1075 [[deprecated("use replaceKey -- it does the same thing")]] QPDF_DLL void
... ...
libqpdf/QPDFAcroFormDocumentHelper.cc
... ... @@ -40,7 +40,7 @@ QPDFAcroFormDocumentHelper::getOrCreateAcroForm()
40 40 {
41 41 auto acroform = this->qpdf.getRoot().getKey("/AcroForm");
42 42 if (!acroform.isDictionary()) {
43   - acroform = this->qpdf.getRoot().replaceKeyAndGet(
  43 + acroform = this->qpdf.getRoot().replaceKeyAndGetNew(
44 44 "/AcroForm",
45 45 this->qpdf.makeIndirectObject(QPDFObjectHandle::newDictionary()));
46 46 }
... ... @@ -53,8 +53,8 @@ QPDFAcroFormDocumentHelper::addFormField(QPDFFormFieldObjectHelper ff)
53 53 auto acroform = getOrCreateAcroForm();
54 54 auto fields = acroform.getKey("/Fields");
55 55 if (!fields.isArray()) {
56   - fields =
57   - acroform.replaceKeyAndGet("/Fields", QPDFObjectHandle::newArray());
  56 + fields = acroform.replaceKeyAndGetNew(
  57 + "/Fields", QPDFObjectHandle::newArray());
58 58 }
59 59 fields.appendItem(ff.getObjectHandle());
60 60 std::set<QPDFObjGen> visited;
... ... @@ -854,7 +854,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
854 854 }
855 855 dr.makeResourcesIndirect(this->qpdf);
856 856 if (!dr.isIndirect()) {
857   - dr = acroform.replaceKeyAndGet(
  857 + dr = acroform.replaceKeyAndGetNew(
858 858 "/DR", this->qpdf.makeIndirectObject(dr));
859 859 }
860 860 // Merge the other document's /DR, creating a conflict
... ... @@ -1076,7 +1076,7 @@ QPDFAcroFormDocumentHelper::transformAnnotations(
1076 1076 auto apdict = ah.getAppearanceDictionary();
1077 1077 std::vector<QPDFObjectHandle> streams;
1078 1078 auto replace_stream = [](auto& dict, auto& key, auto& old) {
1079   - return dict.replaceKeyAndGet(key, old.copyStream());
  1079 + return dict.replaceKeyAndGetNew(key, old.copyStream());
1080 1080 };
1081 1081 if (apdict.isDictionary()) {
1082 1082 for (auto& ap: apdict.ditems()) {
... ...
libqpdf/QPDFEFStreamObjectHelper.cc
... ... @@ -28,7 +28,7 @@ QPDFEFStreamObjectHelper::setParam(
28 28 {
29 29 auto params = this->oh.getDict().getKey("/Params");
30 30 if (!params.isDictionary()) {
31   - params = this->oh.getDict().replaceKeyAndGet(
  31 + params = this->oh.getDict().replaceKeyAndGetNew(
32 32 "/Params", QPDFObjectHandle::newDictionary());
33 33 }
34 34 params.replaceKey(pkey, pval);
... ...
libqpdf/QPDFEmbeddedFileDocumentHelper.cc
... ... @@ -62,8 +62,8 @@ QPDFEmbeddedFileDocumentHelper::initEmbeddedFiles()
62 62 auto root = qpdf.getRoot();
63 63 auto names = root.getKey("/Names");
64 64 if (!names.isDictionary()) {
65   - names =
66   - root.replaceKeyAndGet("/Names", QPDFObjectHandle::newDictionary());
  65 + names = root.replaceKeyAndGetNew(
  66 + "/Names", QPDFObjectHandle::newDictionary());
67 67 }
68 68 auto embedded_files = names.getKey("/EmbeddedFiles");
69 69 if (!embedded_files.isDictionary()) {
... ...
libqpdf/QPDFJob.cc
... ... @@ -2098,7 +2098,7 @@ QPDFJob::doUnderOverlayForPage(
2098 2098 QPDFObjectHandle resources = dest_page.getAttribute("/Resources", true);
2099 2099 if (!resources.isDictionary()) {
2100 2100 QTC::TC("qpdf", "QPDFJob overlay page with no resources");
2101   - resources = dest_page.getObjectHandle().replaceKeyAndGet(
  2101 + resources = dest_page.getObjectHandle().replaceKeyAndGetNew(
2102 2102 "/Resources", QPDFObjectHandle::newDictionary());
2103 2103 }
2104 2104 for (int from_pageno: pagenos[pageno]) {
... ...
libqpdf/QPDFObjectHandle.cc
... ... @@ -915,7 +915,7 @@ QPDFObjectHandle::insertItem(int at, QPDFObjectHandle const&amp; item)
915 915 }
916 916  
917 917 QPDFObjectHandle
918   -QPDFObjectHandle::insertItemAndGet(int at, QPDFObjectHandle const& item)
  918 +QPDFObjectHandle::insertItemAndGetNew(int at, QPDFObjectHandle const& item)
919 919 {
920 920 insertItem(at, item);
921 921 return item;
... ... @@ -934,7 +934,7 @@ QPDFObjectHandle::appendItem(QPDFObjectHandle const&amp; item)
934 934 }
935 935  
936 936 QPDFObjectHandle
937   -QPDFObjectHandle::appendItemAndGet(QPDFObjectHandle const& item)
  937 +QPDFObjectHandle::appendItemAndGetNew(QPDFObjectHandle const& item)
938 938 {
939 939 appendItem(item);
940 940 return item;
... ... @@ -957,7 +957,7 @@ QPDFObjectHandle::eraseItem(int at)
957 957 }
958 958  
959 959 QPDFObjectHandle
960   -QPDFObjectHandle::eraseItemAndGet(int at)
  960 +QPDFObjectHandle::eraseItemAndGetOld(int at)
961 961 {
962 962 auto result = QPDFObjectHandle::newNull();
963 963 if (isArray() && (at < getArrayNItems()) && (at >= 0)) {
... ... @@ -1113,7 +1113,8 @@ QPDFObjectHandle::mergeResources(
1113 1113 // subdictionaries just to get this shallow copy
1114 1114 // functionality.
1115 1115 QTC::TC("qpdf", "QPDFObjectHandle replace with copy");
1116   - this_val = replaceKeyAndGet(rtype, this_val.shallowCopy());
  1116 + this_val =
  1117 + replaceKeyAndGetNew(rtype, this_val.shallowCopy());
1117 1118 }
1118 1119 std::map<QPDFObjGen, std::string> og_to_name;
1119 1120 std::set<std::string> rnames;
... ... @@ -1242,13 +1243,22 @@ QPDFObjectHandle::replaceKey(
1242 1243 }
1243 1244  
1244 1245 QPDFObjectHandle
1245   -QPDFObjectHandle::replaceKeyAndGet(
  1246 +QPDFObjectHandle::replaceKeyAndGetNew(
1246 1247 std::string const& key, QPDFObjectHandle const& value)
1247 1248 {
1248 1249 replaceKey(key, value);
1249 1250 return value;
1250 1251 }
1251 1252  
  1253 +QPDFObjectHandle
  1254 +QPDFObjectHandle::replaceKeyAndGetOld(
  1255 + std::string const& key, QPDFObjectHandle const& value)
  1256 +{
  1257 + QPDFObjectHandle old = removeKeyAndGetOld(key);
  1258 + replaceKey(key, value);
  1259 + return old;
  1260 +}
  1261 +
1252 1262 void
1253 1263 QPDFObjectHandle::removeKey(std::string const& key)
1254 1264 {
... ... @@ -1261,7 +1271,7 @@ QPDFObjectHandle::removeKey(std::string const&amp; key)
1261 1271 }
1262 1272  
1263 1273 QPDFObjectHandle
1264   -QPDFObjectHandle::removeKeyAndGet(std::string const& key)
  1274 +QPDFObjectHandle::removeKeyAndGetOld(std::string const& key)
1265 1275 {
1266 1276 auto result = QPDFObjectHandle::newNull();
1267 1277 if (isDictionary()) {
... ...
libqpdf/QPDFPageObjectHelper.cc
... ... @@ -595,7 +595,7 @@ QPDFPageObjectHelper::removeUnreferencedResourcesHelper(
595 595 for (auto const& iter: to_filter) {
596 596 QPDFObjectHandle dict = resources.getKey(iter);
597 597 if (dict.isDictionary()) {
598   - dict = resources.replaceKeyAndGet(iter, dict.shallowCopy());
  598 + dict = resources.replaceKeyAndGetNew(iter, dict.shallowCopy());
599 599 rdicts.push_back(dict);
600 600 auto keys = dict.getKeys();
601 601 known_names.insert(keys.begin(), keys.end());
... ... @@ -1110,8 +1110,8 @@ QPDFPageObjectHelper::copyAnnotations(
1110 1110 afdh->addAndRenameFormFields(new_fields);
1111 1111 auto annots = this->oh.getKey("/Annots");
1112 1112 if (!annots.isArray()) {
1113   - annots =
1114   - this->oh.replaceKeyAndGet("/Annots", QPDFObjectHandle::newArray());
  1113 + annots = this->oh.replaceKeyAndGetNew(
  1114 + "/Annots", QPDFObjectHandle::newArray());
1115 1115 }
1116 1116 for (auto const& annot: new_annots) {
1117 1117 annots.appendItem(annot);
... ...
libqpdf/QPDFWriter.cc
... ... @@ -1571,7 +1571,7 @@ QPDFWriter::unparseObject(
1571 1571 "qpdf",
1572 1572 "QPDFWriter create Extensions",
1573 1573 this->m->qdf_mode ? 0 : 1);
1574   - extensions = object.replaceKeyAndGet(
  1574 + extensions = object.replaceKeyAndGetNew(
1575 1575 "/Extensions", QPDFObjectHandle::newDictionary());
1576 1576 }
1577 1577 } else if (!have_extensions_other) {
... ... @@ -2277,7 +2277,7 @@ QPDFWriter::prepareFileForWrite()
2277 2277 if (oh.isIndirect()) {
2278 2278 QTC::TC("qpdf", "QPDFWriter make Extensions direct");
2279 2279 extensions_indirect = true;
2280   - oh = root.replaceKeyAndGet(key, oh.shallowCopy());
  2280 + oh = root.replaceKeyAndGetNew(key, oh.shallowCopy());
2281 2281 }
2282 2282 if (oh.hasKey("/ADBE")) {
2283 2283 QPDFObjectHandle adbe = oh.getKey("/ADBE");
... ...
manual/release-notes.rst
... ... @@ -163,9 +163,13 @@ For a detailed list of changes, please see the file
163 163 - See examples :file:`examples/qpdfjob-save-attachment.cc` and
164 164 :file:`examples/qpdfjob-c-save-attachment.cc`.
165 165  
166   - - New methods ``insertItemAndGet``, ``appendItemAndGet``,
167   - ``eraseItemAndGet``, ``replaceKeyAndGet``, and
168   - ``removeKeyAndGet`` return the newly added or removed object.
  166 + - In ``QPDFObjectHandle``, new methods ``insertItemAndGetNew``,
  167 + ``appendItemAndGetNew``, and ``replaceKeyAndGetNew`` return the
  168 + newly added item. New methods ``eraseItemAndGetOld``,
  169 + ``replaceKeyAndGetOld``, and ``removeKeyAndGetOld`` return the
  170 + item that was just removed or, in the case of
  171 + ``replaceKeyAndGetOld``, a ``null`` object if the object was not
  172 + previously there.
169 173  
170 174 - Add new ``Pipeline`` methods to reduce the amount of casting that is
171 175 needed:
... ...
qpdf/test_driver.cc
... ... @@ -1095,8 +1095,8 @@ test_27(QPDF&amp; pdf, char const* arg2)
1095 1095 QPDFObjectHandle s2 = QPDFObjectHandle::newStream(&oldpdf, "potato\n");
1096 1096 auto trailer = pdf.getTrailer();
1097 1097 trailer.replaceKey("/QTest", pdf.copyForeignObject(qtest));
1098   - auto qtest2 =
1099   - trailer.replaceKeyAndGet("/QTest2", QPDFObjectHandle::newArray());
  1098 + auto qtest2 = trailer.replaceKeyAndGetNew(
  1099 + "/QTest2", QPDFObjectHandle::newArray());
1100 1100 qtest2.appendItem(pdf.copyForeignObject(s1));
1101 1101 qtest2.appendItem(pdf.copyForeignObject(s2));
1102 1102 qtest2.appendItem(pdf.copyForeignObject(s3));
... ... @@ -3165,15 +3165,23 @@ test_88(QPDF&amp; pdf, char const* arg2)
3165 3165 auto dict = QPDFObjectHandle::newDictionary();
3166 3166 dict.replaceKey("/One", QPDFObjectHandle::newInteger(1));
3167 3167 dict.replaceKey("/Two", QPDFObjectHandle::newInteger(2));
3168   - auto three = dict.replaceKeyAndGet("/Three", QPDFObjectHandle::newArray());
  3168 + auto three =
  3169 + dict.replaceKeyAndGetNew("/Three", QPDFObjectHandle::newArray());
3169 3170 three.appendItem("(a)"_qpdf);
3170 3171 three.appendItem("(b)"_qpdf);
3171   - auto newdict = three.appendItemAndGet(QPDFObjectHandle::newDictionary());
  3172 + auto newdict = three.appendItemAndGetNew(QPDFObjectHandle::newDictionary());
3172 3173 newdict.replaceKey("/Z", "/Y"_qpdf);
3173 3174 newdict.replaceKey("/X", "/W"_qpdf);
  3175 + dict.replaceKey("/Quack", "[1 2 3]"_qpdf);
  3176 + auto quack = dict.replaceKeyAndGetOld("/Quack", "/Moo"_qpdf);
  3177 + assert(quack.unparse() == "[ 1 2 3 ]");
  3178 + auto nothing =
  3179 + dict.replaceKeyAndGetOld("/NotThere", QPDFObjectHandle::newNull());
  3180 + assert(nothing.isNull());
3174 3181 assert(dict.unparse() == R"(
3175 3182 <<
3176 3183 /One 1
  3184 + /Quack /Moo
3177 3185 /Two 2
3178 3186 /Three [ (a) (b) << /Z /Y /X /W >> ]
3179 3187 >>
... ... @@ -3184,7 +3192,7 @@ test_88(QPDF&amp; pdf, char const* arg2)
3184 3192 assert(
3185 3193 arr.unparse() ==
3186 3194 "[ (00) (0) (a) (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
3187   - auto new_dict = arr.insertItemAndGet(1, "<< /P /Q /R /S >>"_qpdf);
  3195 + auto new_dict = arr.insertItemAndGetNew(1, "<< /P /Q /R /S >>"_qpdf);
3188 3196 arr.eraseItem(2);
3189 3197 arr.eraseItem(0);
3190 3198 assert(
... ... @@ -3200,20 +3208,20 @@ test_88(QPDF&amp; pdf, char const* arg2)
3200 3208 assert(
3201 3209 arr.unparse() ==
3202 3210 "[ << /P /Q /T /U >> (a) (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
3203   - auto s = arr.eraseItemAndGet(1);
  3211 + auto s = arr.eraseItemAndGetOld(1);
3204 3212 assert(s.unparse() == "(a)");
3205 3213 assert(
3206 3214 arr.unparse() ==
3207 3215 "[ << /P /Q /T /U >> (b) << /Z /Y /X /W >> ]"_qpdf.unparse());
3208 3216  
3209   - assert(new_dict.removeKeyAndGet("/M").isNull());
3210   - assert(new_dict.removeKeyAndGet("/P").unparse() == "/Q");
  3217 + assert(new_dict.removeKeyAndGetOld("/M").isNull());
  3218 + assert(new_dict.removeKeyAndGetOld("/P").unparse() == "/Q");
3211 3219 assert(new_dict.unparse() == "<< /T /U >>"_qpdf.unparse());
3212 3220  
3213 3221 // Test errors
3214   - auto arr2 = pdf.getRoot().replaceKeyAndGet("/QTest", "[1 2]"_qpdf);
  3222 + auto arr2 = pdf.getRoot().replaceKeyAndGetNew("/QTest", "[1 2]"_qpdf);
3215 3223 arr2.setObjectDescription(&pdf, "test array");
3216   - assert(arr2.eraseItemAndGet(50).isNull());
  3224 + assert(arr2.eraseItemAndGetOld(50).isNull());
3217 3225 }
3218 3226  
3219 3227 static void
... ...