1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.rat;
20
21 import java.io.File;
22 import java.io.FileFilter;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.io.OutputStreamWriter;
29 import java.io.PrintWriter;
30 import java.net.MalformedURLException;
31 import java.net.URI;
32 import java.net.URL;
33 import java.nio.charset.StandardCharsets;
34 import java.nio.file.Files;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.SortedSet;
41 import java.util.function.Consumer;
42 import java.util.stream.Stream;
43
44 import org.apache.commons.collections4.set.UnmodifiableSortedSet;
45 import org.apache.commons.io.function.IOSupplier;
46 import org.apache.commons.io.output.CloseShieldOutputStream;
47 import org.apache.commons.lang3.StringUtils;
48 import org.apache.rat.analysis.IHeaderMatcher;
49 import org.apache.rat.api.RatException;
50 import org.apache.rat.commandline.StyleSheets;
51 import org.apache.rat.config.AddLicenseHeaders;
52 import org.apache.rat.config.exclusion.ExclusionProcessor;
53 import org.apache.rat.config.exclusion.StandardCollection;
54 import org.apache.rat.config.results.ClaimValidator;
55 import org.apache.rat.configuration.XMLConfigurationReader;
56 import org.apache.rat.configuration.builders.AnyBuilder;
57 import org.apache.rat.document.DocumentName;
58 import org.apache.rat.document.DocumentNameMatcher;
59 import org.apache.rat.document.FileDocument;
60 import org.apache.rat.license.ILicense;
61 import org.apache.rat.license.ILicenseFamily;
62 import org.apache.rat.license.LicenseSetFactory;
63 import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
64 import org.apache.rat.report.RatReport;
65 import org.apache.rat.report.Reportable;
66 import org.apache.rat.report.claim.ClaimStatistic;
67 import org.apache.rat.report.xml.writer.XmlWriter;
68 import org.apache.rat.utils.DefaultLog;
69 import org.apache.rat.utils.Log.Level;
70 import org.apache.rat.utils.ReportingSet;
71 import org.apache.rat.utils.StandardXmlFactory;
72 import org.apache.rat.walker.FileListWalker;
73 import org.apache.rat.walker.ReportableListWalker;
74 import org.w3c.dom.Node;
75 import org.xml.sax.SAXException;
76
77 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
78
79
80
81
82
83
84 public class ReportConfiguration {
85
86
87 public static final IODescriptor<OutputStream> SYSTEM_OUT =
88
89 new IODescriptor<>("System.out", () -> CloseShieldOutputStream.wrap(System.out));
90
91
92
93
94 public enum Processing {
95
96 NOTIFICATION("List file as present"),
97
98 PRESENCE("List any licenses found"),
99
100 ABSENCE("List licenses found and any unknown licences");
101
102
103
104
105 private final String description;
106
107 Processing(final String description) {
108 this.description = description;
109 }
110
111
112
113
114
115 public String desc() {
116 return description;
117 }
118 }
119
120
121 private final LicenseSetFactory licenseSetFactory;
122
123
124
125
126 private boolean addingLicenses;
127
128
129
130
131 private boolean addingLicensesForced;
132
133
134
135
136
137 private String copyrightMessage;
138
139
140
141
142 private IODescriptor<OutputStream> out;
143
144
145
146
147 private IODescriptor<InputStream> styleSheet;
148
149
150
151
152 private final List<File> sources;
153
154
155
156
157 private final List<Reportable> reportables;
158
159
160
161
162 private final ExclusionProcessor exclusionProcessor;
163
164
165
166
167 private LicenseFilter listFamilies;
168
169
170
171
172 private LicenseFilter listLicenses;
173
174
175
176
177 private boolean dryRun;
178
179
180
181
182 private Processing archiveProcessing;
183
184
185
186
187 private Processing standardProcessing;
188
189
190
191
192 private final ClaimValidator claimValidator;
193
194
195
196
197 public ReportConfiguration() {
198 licenseSetFactory = new LicenseSetFactory();
199 listFamilies = Defaults.LIST_FAMILIES;
200 listLicenses = Defaults.LIST_LICENSES;
201 dryRun = false;
202 exclusionProcessor = new ExclusionProcessor();
203 claimValidator = new ClaimValidator();
204 sources = new ArrayList<>();
205 reportables = new ArrayList<>();
206 }
207
208 public SerDes serDes() {
209 return new SerDes();
210 }
211
212
213
214
215
216 public void reportExclusions(final Appendable appendable) {
217 try {
218 exclusionProcessor.reportExclusions(appendable);
219 } catch (IOException e) {
220 DefaultLog.getInstance().warn("Unable to report exclusions", e);
221 }
222 }
223
224
225
226
227
228
229
230
231 public void addSource(final File file) {
232 notNull(file, "File may not be null.");
233 sources.add(file);
234 }
235
236 private void notNull(final Object o, final String msg) {
237 if (o == null) {
238 throw new ConfigurationException(msg);
239 }
240 }
241
242
243
244
245
246 public void addSource(final Reportable reportable) {
247 notNull(reportable, "Reportable may not be null.");
248 reportables.add(reportable);
249 }
250
251
252
253
254
255 public boolean hasSource() {
256 return !reportables.isEmpty() || !sources.isEmpty();
257 }
258
259
260
261
262
263 public ReportableListWalker.Builder getSources() {
264 DocumentName name = DocumentName.builder(new File(".")).build();
265 ReportableListWalker.Builder builder = ReportableListWalker.builder(name);
266 sources.forEach(file -> builder.addReportable(new FileListWalker(new FileDocument(file, DocumentNameMatcher.MATCHES_ALL))));
267 reportables.forEach(builder::addReportable);
268 return builder;
269 }
270
271
272 Iterable<File> sources() {
273 return sources;
274 }
275
276
277 Stream<DocumentName> reportables() {
278 return reportables.stream().map(Reportable::name);
279 }
280
281
282
283
284
285 public IHeaderMatcher getGeneratedMatcher() {
286 return new AnyBuilder().setResource("/org/apache/rat/generation-keywords.txt").build();
287 }
288
289
290
291
292
293 public Processing getArchiveProcessing() {
294 return archiveProcessing == null ? Defaults.ARCHIVE_PROCESSING : archiveProcessing;
295 }
296
297
298
299
300
301 public void setArchiveProcessing(final Processing archiveProcessing) {
302 this.archiveProcessing = archiveProcessing;
303 }
304
305
306
307
308
309 public Processing getStandardProcessing() {
310 return standardProcessing == null ? Defaults.STANDARD_PROCESSING : standardProcessing;
311 }
312
313
314
315
316
317 public void setStandardProcessing(final Processing standardProcessing) {
318 this.standardProcessing = standardProcessing;
319 }
320
321
322
323
324
325
326 public void logFamilyCollisions(final Level level) {
327 licenseSetFactory.logFamilyCollisions(level);
328 }
329
330
331
332
333
334 public void familyDuplicateOption(final ReportingSet.Options state) {
335 licenseSetFactory.familyDuplicateOption(state);
336 }
337
338
339
340
341
342 public void logLicenseCollisions(final Level level) {
343 licenseSetFactory.logLicenseCollisions(level);
344 }
345
346
347
348
349
350 public void licenseDuplicateOption(final ReportingSet.Options state) {
351 licenseSetFactory.licenseDuplicateOption(state);
352 }
353
354
355
356
357
358 public void listFamilies(final LicenseFilter filter) {
359 listFamilies = filter;
360 }
361
362
363
364
365
366 public LicenseFilter listFamilies() {
367 return listFamilies;
368 }
369
370
371
372
373
374 public void listLicenses(final LicenseFilter filter) {
375 listLicenses = filter;
376 }
377
378
379
380
381
382 public LicenseFilter listLicenses() {
383 return listLicenses;
384 }
385
386
387
388
389
390 public void setDryRun(final boolean state) {
391 dryRun = state;
392 }
393
394
395
396
397
398 public boolean isDryRun() {
399 return dryRun;
400 }
401
402
403
404
405
406
407 public void addExcludedCollection(final StandardCollection collection) {
408 exclusionProcessor.addExcludedCollection(collection);
409 }
410
411
412
413
414
415
416 public void addExcludedFileProcessor(final StandardCollection collection) {
417 exclusionProcessor.addFileProcessor(collection);
418 }
419
420
421
422
423
424 public void addExcludedFilter(final FileFilter fileFilter) {
425 exclusionProcessor.addExcludedMatcher(new DocumentNameMatcher(fileFilter));
426 }
427
428
429
430
431
432 public void addExcludedMatcher(final DocumentNameMatcher matcher) {
433 exclusionProcessor.addExcludedMatcher(matcher);
434 }
435
436
437
438
439
440
441
442 public void addExcludedPatterns(final Iterable<String> patterns) {
443 exclusionProcessor.addExcludedPatterns(patterns);
444 }
445
446
447
448
449
450 public void addIncludedCollection(final StandardCollection collection) {
451 exclusionProcessor.addIncludedCollection(collection);
452 }
453
454
455
456
457
458
459 public void addIncludedFilter(final FileFilter fileFilter) {
460 exclusionProcessor.addIncludedMatcher(new DocumentNameMatcher(fileFilter));
461 }
462
463
464
465
466
467 public void addIncludedMatcher(final DocumentNameMatcher matcher) {
468 exclusionProcessor.addIncludedMatcher(matcher);
469 }
470
471
472
473
474
475
476 public void addIncludedPatterns(final Iterable<String> patterns) {
477 exclusionProcessor.addIncludedPatterns(patterns);
478 }
479
480
481
482
483
484
485 public DocumentNameMatcher getDocumentExcluder(final DocumentName baseDir) {
486 return exclusionProcessor.getNameMatcher(baseDir);
487 }
488
489
490 ExclusionProcessor getExclusionProcessor() {
491 return exclusionProcessor;
492 }
493
494
495
496
497
498
499 public IOSupplier<InputStream> getStyleSheet() {
500 return styleSheet == null ? null : styleSheet.ioSupplier();
501 }
502
503
504
505
506
507
508 public IODescriptor<InputStream> getStyleSheetDescriptor() {
509 return styleSheet == null ? null : styleSheet;
510 }
511
512
513
514
515
516
517
518 public void setStyleSheet(final IODescriptor<InputStream> styleSheet) {
519 this.styleSheet = styleSheet;
520 }
521
522
523
524
525
526
527
528 public void setFrom(final Defaults defaults) {
529 licenseSetFactory.add(defaults.getLicenseSetFactory());
530 if (getStyleSheet() == null) {
531 setStyleSheet(StyleSheets.PLAIN.getStyleSheet());
532 }
533 defaults.getStandardExclusion().forEach(this::addExcludedCollection);
534 }
535
536
537
538
539
540 public void setStyleSheet(final File styleSheet) {
541 Objects.requireNonNull(styleSheet, "styleSheet file should not be null");
542 setStyleSheet(styleSheet.toURI());
543 }
544
545
546
547
548
549
550 public void setStyleSheet(final URI styleSheet) {
551 Objects.requireNonNull(styleSheet, "Stylesheet file must not be null");
552 try {
553 setStyleSheet(styleSheet.toURL());
554 } catch (MalformedURLException e) {
555 throw new ConfigurationException("Unable to process stylesheet", e);
556 }
557 }
558
559
560
561
562
563
564 public void setStyleSheet(final URL styleSheet) {
565 Objects.requireNonNull(styleSheet, "Stylesheet file must not be null");
566 setStyleSheet(new IODescriptor<>(styleSheet.toString(), styleSheet::openStream));
567 }
568
569
570
571
572
573
574
575
576
577
578 public void setOut(final IODescriptor<OutputStream> out) {
579 this.out = out;
580 }
581
582
583
584
585
586
587
588
589 public void setOut(final File file) {
590 Objects.requireNonNull(file, "output file should not be null");
591 if (file.exists()) {
592 try {
593 Files.delete(file.toPath());
594 } catch (IOException e) {
595 DefaultLog.getInstance().warn("Unable to delete file: " + file);
596 }
597 }
598
599 File parent = file.getParentFile();
600 if (!parent.mkdirs() && !parent.isDirectory()) {
601 DefaultLog.getInstance().warn("Unable to create directory: " + file.getParentFile());
602 }
603 setOut(IODescriptor.output(file));
604 }
605
606
607
608
609
610
611 public IOSupplier<OutputStream> getOutput() {
612 return getOutputDescriptor().ioSupplier();
613 }
614
615
616
617
618
619
620 public IODescriptor<OutputStream> getOutputDescriptor() {
621 return out == null ? SYSTEM_OUT : out;
622 }
623
624
625
626
627
628
629 public IOSupplier<PrintWriter> getWriter() {
630 return () -> new PrintWriter(new OutputStreamWriter(getOutput().get(), StandardCharsets.UTF_8));
631 }
632
633
634
635
636
637
638 public void addLicense(final ILicense license) {
639 licenseSetFactory.addLicense(license);
640 }
641
642
643
644
645
646
647
648 public ILicense addLicense(final ILicense.Builder builder) {
649 return licenseSetFactory.addLicense(builder);
650 }
651
652
653
654
655
656
657 public void addLicenses(final Collection<ILicense> licenses) {
658 licenseSetFactory.addLicenses(licenses);
659 }
660
661
662
663
664
665
666 public void addFamily(final ILicenseFamily family) {
667 licenseSetFactory.addFamily(family);
668 }
669
670
671
672
673
674
675
676 public void addFamily(final ILicenseFamily.Builder builder) {
677 licenseSetFactory.addFamily(builder);
678 }
679
680
681
682
683
684
685 public void addFamilies(final Collection<ILicenseFamily> families) {
686 families.forEach(this::addApprovedLicenseCategory);
687 }
688
689
690
691
692
693 public void addApprovedLicenseCategory(final ILicenseFamily approvedILicenseFamily) {
694 addApprovedLicenseCategory(approvedILicenseFamily.getFamilyCategory());
695 }
696
697
698
699
700
701 public void addApprovedLicenseCategory(final String familyCategory) {
702 licenseSetFactory.approveLicenseCategory(familyCategory);
703 }
704
705
706
707
708
709
710 public void addApprovedLicenseCategories(final Collection<String> approvedLicenseCategories) {
711 approvedLicenseCategories.forEach(this::addApprovedLicenseCategory);
712 }
713
714
715
716
717
718
719 public void removeApprovedLicenseCategory(final String familyCategory) {
720 licenseSetFactory.removeLicenseCategory(ILicenseFamily.makeCategory(familyCategory));
721 }
722
723
724
725
726
727
728
729 public void removeApprovedLicenseCategories(final Collection<String> familyCategory) {
730 familyCategory.forEach(this::removeApprovedLicenseCategory);
731 }
732
733
734
735
736
737
738
739 public SortedSet<String> getLicenseCategories(final LicenseFilter filter) {
740 return licenseSetFactory.getLicenseCategories(filter);
741 }
742
743
744
745
746
747
748
749 public UnmodifiableSortedSet<ILicense> getLicenses(final LicenseFilter filter) {
750 return licenseSetFactory.getLicenses(filter);
751 }
752
753
754
755
756
757
758
759 public SortedSet<String> getLicenseIds(final LicenseFilter filter) {
760 return licenseSetFactory.getLicenseIds(filter);
761 }
762
763
764
765
766
767 public void addApprovedLicenseId(final ILicense approvedLicense) {
768 addApprovedLicenseId(approvedLicense.getId());
769 }
770
771
772
773
774
775 public void addApprovedLicenseId(final String licenseId) {
776 licenseSetFactory.approveLicenseId(licenseId);
777 }
778
779
780
781
782
783
784 public void addApprovedLicenseIds(final Collection<String> approvedLicenseIds) {
785 approvedLicenseIds.forEach(this::addApprovedLicenseId);
786 }
787
788
789
790
791
792
793 public void removeApprovedLicenseId(final String licenseId) {
794 licenseSetFactory.removeLicenseId(licenseId);
795 }
796
797
798
799
800
801
802
803 public void removeApprovedLicenseIds(final Collection<String> licenseIds) {
804 licenseIds.forEach(this::removeApprovedLicenseId);
805 }
806
807
808
809
810
811
812
813 public String getCopyrightMessage() {
814 return copyrightMessage;
815 }
816
817
818
819
820
821
822
823 public void setCopyrightMessage(final String copyrightMessage) {
824 this.copyrightMessage = copyrightMessage;
825 }
826
827
828
829
830
831
832
833 public boolean isAddingLicensesForced() {
834 return addingLicensesForced;
835 }
836
837
838
839
840
841
842
843 public boolean isAddingLicenses() {
844 return addingLicenses;
845 }
846
847
848
849
850
851
852
853
854
855 public void setAddLicenseHeaders(final AddLicenseHeaders addLicenseHeaders) {
856 addingLicenses = false;
857 addingLicensesForced = false;
858 switch (addLicenseHeaders) {
859 case FALSE:
860
861 break;
862 case FORCED:
863 addingLicensesForced = true;
864 addingLicenses = true;
865 break;
866 case TRUE:
867 addingLicenses = true;
868 break;
869 }
870 }
871
872
873
874
875
876
877
878
879
880
881
882
883 public SortedSet<ILicenseFamily> getLicenseFamilies(final LicenseFilter filter) {
884 return licenseSetFactory.getLicenseFamilies(filter);
885 }
886
887
888
889
890
891 public ClaimValidator getClaimValidator() {
892 return claimValidator;
893 }
894
895
896
897
898
899 public LicenseSetFactory getLicenseSetFactory() {
900 return licenseSetFactory;
901 }
902
903
904
905
906
907
908 public void validate(final Consumer<String> logger) {
909 if (!hasSource()) {
910 String msg = "At least one source must be specified";
911 logger.accept(msg);
912 throw new ConfigurationException(msg);
913 }
914 if (licenseSetFactory.getLicenses(LicenseFilter.ALL).isEmpty()) {
915 String msg = "You must specify at least one license";
916 logger.accept(msg);
917 throw new ConfigurationException(msg);
918 }
919 }
920
921
922
923
924
925
926
927 public record IODescriptor<T>(String name, IOSupplier<T> ioSupplier) {
928
929
930
931
932
933
934
935
936 static IODescriptor<OutputStream> output(final String name, final DocumentName workingDirectory) {
937 DocumentName docName = workingDirectory.resolve(name);
938 return new IODescriptor<>(name, () -> new FileOutputStream(docName.asFile()));
939 }
940
941
942
943
944
945
946 static IODescriptor<OutputStream> output(final File file) {
947 return new IODescriptor<>(file.toString(), () -> new FileOutputStream(file, true));
948 }
949
950
951
952
953
954
955
956 static IODescriptor<InputStream> input(final File file) {
957 return new IODescriptor<>(file.toString(), () -> new FileInputStream(file));
958 }
959 }
960
961
962
963
964
965
966 @SuppressFBWarnings("EI_EXPOSE_REP2")
967 public class SerDes {
968
969
970
971
972
973
974 public void serialize(final Appendable appendable) throws IOException {
975 try (XmlWriter writer = new XmlWriter(appendable)) {
976 writer.startElement("ReportConfiguration")
977 .attribute("addingLicenses", Boolean.toString(addingLicenses))
978 .attribute("addingLicensesForced", Boolean.toString(addingLicensesForced))
979 .attribute("listFamilies", listFamilies.name())
980 .attribute("listLicenses", listLicenses.name())
981 .attribute("dryRun", Boolean.toString(dryRun))
982 .attribute("archiveProcessing", getArchiveProcessing().name())
983 .attribute("standardProcessing", getStandardProcessing().name())
984 .attribute("stylesheet", styleSheet.name())
985 .attribute("output", out.name());
986 if (StringUtils.isNotEmpty(copyrightMessage)) {
987 writer.startElement("copyrightMessage").content(copyrightMessage).closeElement();
988 }
989 writer.startElement("sources");
990 for (File f : sources) {
991 writer.startElement("source").attribute("name", f.toString()).closeElement();
992 }
993 writer.closeElement("sources").startElement("reportables");
994 for (Reportable reportable : reportables) {
995 writer.startElement("reportable")
996 .attribute("baseName", reportable.name().getBaseName())
997 .attribute("name", reportable.name().toString())
998 .attribute("class", reportable.getClass().getName()).closeElement();
999 }
1000 writer.closeElement();
1001
1002 exclusionProcessor.serDes().serialize(writer);
1003
1004 writer.startElement("claimValidator");
1005 for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) {
1006 writer.startElement("claimCounter")
1007 .attribute("name", counter.name()).attribute("min", Integer.toString(claimValidator.getMin(counter)))
1008 .attribute("max", Integer.toString(claimValidator.getMax(counter))).closeElement();
1009 }
1010 writer.closeElement();
1011 } catch (IOException e) {
1012 throw e;
1013 } catch (Exception e) {
1014 throw new IOException(e);
1015 }
1016 }
1017
1018 public void deserialize(final IOSupplier<InputStream> inputStreamSupplier, final DocumentName workingDirectory) throws IOException {
1019 org.w3c.dom.Document document;
1020 try (InputStream stream = inputStreamSupplier.get()) {
1021 document = StandardXmlFactory.documentBuilder().parse(stream);
1022 } catch (SAXException e) {
1023 throw new IOException("Unable to read input", e);
1024 }
1025 Node node = document.getDocumentElement();
1026 if (!node.getNodeName().equals("ReportConfiguration")) {
1027 throw new IOException("Invalid ReportConfiguration");
1028 }
1029 Map<String, String> attributes = XMLConfigurationReader.attributes(node);
1030 addingLicenses = Boolean.parseBoolean(attributes.get("addingLicenses"));
1031 addingLicensesForced = Boolean.parseBoolean(attributes.get("addingLicensesForced"));
1032 listFamilies = LicenseFilter.valueOf(attributes.get("listFamilies"));
1033 listLicenses = LicenseFilter.valueOf(attributes.get("listLicenses"));
1034 dryRun = Boolean.parseBoolean(attributes.get("dryRun"));
1035 archiveProcessing = Processing.valueOf(attributes.get("archiveProcessing"));
1036 standardProcessing = Processing.valueOf(attributes.get("standardProcessing"));
1037 String styleName = attributes.get("stylesheet");
1038 if (styleName != null) {
1039 styleSheet = StyleSheets.getStyleSheet(styleName);
1040 }
1041 String outputName = attributes.get("output");
1042 if (outputName != null) {
1043 if (outputName.equals(ReportConfiguration.SYSTEM_OUT.name())) {
1044 out = ReportConfiguration.SYSTEM_OUT;
1045 } else {
1046 out = IODescriptor.output(outputName, workingDirectory);
1047 }
1048 }
1049
1050 XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("copyrightMessage"),
1051 lNode -> setCopyrightMessage(lNode.getTextContent()));
1052
1053 XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("source"), lNode -> {
1054 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1055 addSource(new File(nAttributes.get("name")));
1056 });
1057
1058
1059 XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("reportable"), lNode -> {
1060 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1061 DocumentName documentName = DocumentName.builder().setBaseName(nAttributes.get("baseName"))
1062 .setName(nAttributes.get("name")).build();
1063 addSource(new DeserializedReportable(documentName));
1064 });
1065
1066 exclusionProcessor.serDes().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0));
1067
1068 XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("claimCounter"), lNode -> {
1069 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1070 ClaimStatistic.Counter counter = ClaimStatistic.Counter.valueOf(nAttributes.get("name"));
1071 claimValidator.setMin(counter, Integer.parseInt(nAttributes.get("min")));
1072 claimValidator.setMax(counter, Integer.parseInt(nAttributes.get("max")));
1073 });
1074 }
1075 }
1076
1077
1078
1079
1080
1081 private record DeserializedReportable(DocumentName name) implements Reportable {
1082 @Override
1083 public void run(final RatReport report) throws RatException {
1084 throw new RatException("Attempt to run a deserialized reportable");
1085 }
1086 }
1087 }