Skip to contentMethod: static {...}
1: /**
2: * Copyright (C) 2022 Czech Technical University in Prague
3: * <p>
4: * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
5: * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
6: * version.
7: * <p>
8: * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
9: * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
10: * details. You should have received a copy of the GNU General Public License along with this program. If not, see
11: * <http://www.gnu.org/licenses/>.
12: */
13: package cz.cvut.kbss.jopa.owl2java;
14:
15: import com.sun.codemodel.*;
16: import cz.cvut.kbss.jopa.ic.api.AtomicSubClassConstraint;
17: import cz.cvut.kbss.jopa.ic.api.DataParticipationConstraint;
18: import cz.cvut.kbss.jopa.ic.api.ObjectParticipationConstraint;
19: import cz.cvut.kbss.jopa.model.MultilingualString;
20: import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty;
21: import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty;
22: import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty;
23: import cz.cvut.kbss.jopa.model.annotations.Properties;
24: import cz.cvut.kbss.jopa.model.annotations.*;
25: import cz.cvut.kbss.jopa.owl2java.cli.Option;
26: import cz.cvut.kbss.jopa.owl2java.cli.PropertiesType;
27: import cz.cvut.kbss.jopa.owl2java.config.TransformationConfiguration;
28: import cz.cvut.kbss.jopa.owl2java.exception.OWL2JavaException;
29: import cz.cvut.kbss.jopa.owlapi.DatatypeTransformer;
30: import cz.cvut.kbss.jopa.vocabulary.DC;
31: import cz.cvut.kbss.jopa.vocabulary.RDFS;
32: import org.semanticweb.owlapi.model.OWLClass;
33: import org.semanticweb.owlapi.model.*;
34: import org.semanticweb.owlapi.search.EntitySearcher;
35: import org.slf4j.Logger;
36: import org.slf4j.LoggerFactory;
37:
38: import java.io.Serializable;
39: import java.text.Normalizer;
40: import java.util.*;
41: import java.util.concurrent.atomic.AtomicBoolean;
42: import java.util.stream.Collectors;
43:
44: import static cz.cvut.kbss.jopa.owl2java.Constants.*;
45:
46: public class JavaTransformer {
47:
48: private static final Logger LOG = LoggerFactory.getLogger(OWL2JavaTransformer.class);
49:
50: private static final String[] KEYWORDS = {"abstract",
51: "assert",
52: "boolean",
53: "break",
54: "byte",
55: "case",
56: "catch",
57: "char",
58: "class",
59: "const",
60: "continue",
61: "default",
62: "do",
63: "double",
64: "else",
65: "enum",
66: "extends",
67: "final",
68: "finally",
69: "float",
70: "for",
71: "goto",
72: "if",
73: "implements",
74: "import",
75: "instanceof",
76: "int",
77: "interface",
78: "long",
79: "native",
80: "new",
81: "package",
82: "private",
83: "protected",
84: "public",
85: "return",
86: "short",
87: "static",
88: "strictfp",
89: "super",
90: "switch",
91: "synchronized",
92: "this",
93: "throw",
94: "throws",
95: "transient",
96: "try",
97: "void",
98: "volatile",
99: "while"};
100:
101: private static final String PREFIX_STRING = "s_";
102: private static final String PREFIX_CLASS = "c_";
103: private static final String PREFIX_PROPERTY = "p_";
104: private static final String PREFIX_INDIVIDUAL = "i_";
105: private static final String PREFIX_DATATYPE = "d_";
106:
107: private JDefinedClass voc;
108: private final Map<OWLEntity, JFieldRef> entities = new HashMap<>();
109:
110: private final Map<OWLClass, JDefinedClass> classes = new HashMap<>();
111:
112: private final TransformationConfiguration configuration;
113:
114: JavaTransformer(TransformationConfiguration configuration) {
115: this.configuration = configuration;
116: }
117:
118: private static String validJavaIDForIRI(final IRI iri) {
119: if (iri.getFragment() != null) {
120: return validJavaID(iri.getFragment());
121: } else {
122: int x = iri.toString().lastIndexOf("/");
123: return validJavaID(iri.toString().substring(x + 1));
124: }
125: }
126:
127: private static String validJavaID(final String s) {
128: String res = s.trim().replace("-", "_").replace("'", "_quote_").replace(".", "_dot_").replace(',', '_');
129: // Replace non-ASCII characters with ASCII ones
130: res = Normalizer.normalize(res, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
131: if (Arrays.binarySearch(KEYWORDS, res) >= 0) {
132: res = "_" + res;
133: }
134: return res;
135: }
136:
137: private static JFieldVar addField(final String name, final JDefinedClass cls,
138: final JType fieldType) {
139: String newName = name;
140:
141: int i = 0;
142: while (cls.fields().containsKey(newName)) {
143: newName = name + "" + (++i);
144: }
145:
146: final JFieldVar fvId = cls.field(JMod.PROTECTED, fieldType, newName);
147: final String fieldName = fvId.name().substring(0, 1).toUpperCase() + fvId.name().substring(1);
148: final JMethod mSetId = cls.method(JMod.PUBLIC, void.class, "set" + fieldName);
149: final JVar v = mSetId.param(fieldType, fvId.name());
150: mSetId.body().assign(JExpr._this().ref(fvId), v);
151: final JMethod mGetId = cls.method(JMod.PUBLIC, fieldType, "get" + fieldName);
152: mGetId.body()._return(fvId);
153: return fvId;
154: }
155:
156: /**
157: * Generates an object model consisting of JOPA entity classes and a vocabulary file from the specified ontology and
158: * context definition.
159: *
160: * @param ontology Ontology from which the model is generated
161: * @param context Context information
162: * @return The generated object model
163: */
164: public ObjectModel generateModel(final OWLOntology ontology, final ContextDefinition context) {
165: try {
166: final JCodeModel cm = new JCodeModel();
167: voc = createVocabularyClass(cm);
168: generateVocabulary(ontology, cm, context);
169: generateModelImpl(ontology, cm, context, modelPackageName());
170: return new ObjectModel(cm);
171: } catch (JClassAlreadyExistsException e) {
172: throw new OWL2JavaException("Transformation FAILED.", e);
173: }
174: }
175:
176: private String modelPackageName() {
177: final String packageConfig = configuration.getPackageName();
178: final StringBuilder sb = new StringBuilder(packageConfig);
179: if (!packageConfig.isEmpty()) {
180: sb.append(PACKAGE_SEPARATOR);
181: }
182: sb.append(MODEL_PACKAGE);
183: return sb.toString();
184: }
185:
186: private JDefinedClass createVocabularyClass(JCodeModel codeModel)
187: throws JClassAlreadyExistsException {
188: final String packageName = configuration.getPackageName();
189: final String className =
190: packageName.isEmpty() ? VOCABULARY_CLASS : packageName + PACKAGE_SEPARATOR + VOCABULARY_CLASS;
191: final JDefinedClass cls = codeModel._class(className);
192: generateAuthorshipDoc(cls);
193: return cls;
194: }
195:
196: private static void generateAuthorshipDoc(JDocCommentable javaElem) {
197: javaElem.javadoc().add("This class was generated by OWL2Java " + VERSION);
198: }
199:
200: /**
201: * Generates only vocabulary of the loaded ontology.
202: *
203: * @param ontology Ontology from which the vocabulary should be generated
204: * @param context Integrity constraints context, if null is supplied, the whole ontology is interpreted as
205: * integrity constraints.
206: * @return The generated object model
207: */
208: public ObjectModel generateVocabulary(final OWLOntology ontology, ContextDefinition context) {
209: try {
210: final JCodeModel cm = new JCodeModel();
211: this.voc = createVocabularyClass(cm);
212: generateVocabulary(ontology, cm, context);
213: return new ObjectModel(cm);
214: } catch (JClassAlreadyExistsException e) {
215: throw new OWL2JavaException("Vocabulary generation FAILED, because the Vocabulary class already exists.",
216: e);
217: }
218: }
219:
220: private void generateObjectProperty(final OWLOntology ontology,
221: final JCodeModel cm,
222: final ContextDefinition context,
223: final String pkg,
224: final OWLClass clazz,
225: final JDefinedClass subj,
226: final org.semanticweb.owlapi.model.OWLObjectProperty prop) {
227: final ClassObjectPropertyComputer comp = new ClassObjectPropertyComputer(
228: clazz,
229: prop,
230: context.set,
231: ontology
232: );
233:
234: if (Card.NO != comp.getCard()) {
235: JClass filler = ensureCreated(pkg, cm, comp.getFiller(), ontology);
236: final String fieldName = validJavaIDForIRI(prop.getIRI());
237:
238: switch (comp.getCard()) {
239: case MULTIPLE:
240: filler = cm.ref(java.util.Set.class).narrow(filler);
241: break;
242: case SIMPLELIST:
243: case LIST:
244: filler = cm.ref(java.util.List.class).narrow(filler);
245: break;
246: default:
247: break;
248: }
249:
250: final JFieldVar fv = addField(fieldName, subj, filler);
251: generateJavadoc(ontology, prop, fv);
252:
253: if (comp.getCard().equals(Card.SIMPLELIST)) {
254: fv.annotate(Sequence.class).param("type", SequenceType.simple);
255: }
256:
257:
258: fv.annotate(OWLObjectProperty.class).param("iri", entities.get(prop));
259:
260: JAnnotationArrayMember use = null;
261: for (ObjectParticipationConstraint ic : comp.getParticipationConstraints()) {
262: if (use == null) {
263: use = fv.annotate(ParticipationConstraints.class).paramArray("value");
264: }
265: JAnnotationUse u = use.annotate(ParticipationConstraint.class)
266: .param("owlObjectIRI", entities.get(ic.getObject()));
267: setParticipationConstraintCardinality(u, ic);
268: }
269: }
270: }
271:
272: private static void setParticipationConstraintCardinality(JAnnotationUse u,
273: cz.cvut.kbss.jopa.ic.api.ParticipationConstraint<?, ?> ic) {
274: if (ic.getMin() != 0) {
275: u.param("min", ic.getMin());
276: }
277: if (ic.getMin() != -1) {
278: u.param("max", ic.getMax());
279: }
280: }
281:
282: private void generateDataProperty(final OWLOntology ontology,
283: final JCodeModel cm,
284: final ContextDefinition context,
285: final OWLClass clazz,
286: final JDefinedClass subj,
287: final org.semanticweb.owlapi.model.OWLDataProperty prop) {
288: final ClassDataPropertyComputer comp = new ClassDataPropertyComputer(
289: clazz,
290: prop,
291: context.set,
292: ontology
293: );
294:
295: if (Card.NO != comp.getCard()) {
296:
297: final JType obj = cm._ref(resolveFieldType(comp.getFiller()));
298:
299: final String fieldName = validJavaIDForIRI(prop.getIRI());
300:
301: JFieldVar fv;
302:
303: switch (comp.getCard()) {
304: case MULTIPLE:
305: fv = addField(fieldName, subj, cm.ref(java.util.Set.class).narrow(obj));
306: break;
307: case ONE:
308: fv = addField(fieldName, subj, obj);
309: break;
310: default:
311: throw new OWL2JavaException("Unsupported data property cardinality type " + comp.getCard());
312: }
313: generateJavadoc(ontology, prop, fv);
314:
315: fv.annotate(OWLDataProperty.class).param("iri", entities.get(prop));
316:
317: JAnnotationArrayMember use = null;
318: for (DataParticipationConstraint ic : comp.getParticipationConstraints()) {
319: if (use == null) {
320: use = fv.annotate(ParticipationConstraints.class).paramArray("value");
321: }
322:
323: JAnnotationUse u = use.annotate(ParticipationConstraint.class)
324: .param("owlObjectIRI", comp.getFiller().getIRI().toString());
325:
326: setParticipationConstraintCardinality(u, ic);
327: }
328: }
329: }
330:
331: private Class<?> resolveFieldType(OWLDatatype datatype) {
332: final Class<?> cls = DatatypeTransformer.transformOWLType(datatype);
333: if (MultilingualString.class.equals(cls) && !configuration.shouldPreferMultilingualStrings()) {
334: return String.class;
335: }
336: return cls;
337: }
338:
339: private void generateModelImpl(final OWLOntology ontology, final JCodeModel cm,
340: final ContextDefinition context, final String pkg) {
341: LOG.info("Generating model ...");
342: final PropertiesType propertiesType = configuration.getPropertiesType();
343:
344: if (configuration.shouldGenerateThing()) {
345: context.classes.add(ontology.getOWLOntologyManager().getOWLDataFactory().getOWLThing());
346: }
347:
348: for (final OWLClass clazz : context.classes) {
349: LOG.info(" Generating class '{}'.", clazz);
350: final JDefinedClass subj = ensureCreated(pkg, cm, clazz, ontology);
351:
352: final AtomicBoolean extendClass = new AtomicBoolean(false);
353: context.set.getClassIntegrityConstraints(clazz).stream()
354: .filter(ic -> ic instanceof AtomicSubClassConstraint).forEach(ic -> {
355: final AtomicSubClassConstraint icc = (AtomicSubClassConstraint) ic;
356: subj._extends(ensureCreated(pkg, cm, icc.getSupClass(), ontology));
357: extendClass.set(true);
358: });
359:
360: if (!extendClass.get()) {
361: addCommonClassFields(cm, subj, propertiesType);
362: }
363: for (final org.semanticweb.owlapi.model.OWLObjectProperty prop : context.objectProperties) {
364: generateObjectProperty(ontology, cm, context, pkg, clazz, subj, prop);
365: }
366:
367: for (org.semanticweb.owlapi.model.OWLDataProperty prop : context.dataProperties) {
368: generateDataProperty(ontology, cm, context, clazz, subj, prop);
369: }
370: }
371: }
372:
373: private void generateVocabulary(final OWLOntology o, final JCodeModel cm, ContextDefinition context) {
374: final Collection<OWLEntity> col = new LinkedHashSet<>();
375: col.add(o.getOWLOntologyManager().getOWLDataFactory().getOWLThing());
376: col.addAll(context.classes);
377: col.addAll(context.objectProperties);
378: col.addAll(context.dataProperties);
379: col.addAll(context.annotationProperties);
380: col.addAll(context.individuals);
381:
382: generateOntologyIrisConstants(o.getOWLOntologyManager());
383:
384: final Set<IRI> visitedProperties = new HashSet<>(col.size());
385:
386: for (final OWLEntity c : col) {
387: final Optional<String> prefix = resolveFieldPrefix(c, visitedProperties);
388: if (!prefix.isPresent()) {
389: continue;
390: }
391: final String sFieldName = ensureUniqueIdentifier(
392: PREFIX_STRING + prefix.get() + validJavaIDForIRI(c.getIRI()));
393:
394: final JFieldVar fv1 = voc.field(JMod.PUBLIC | JMod.STATIC
395: | JMod.FINAL, String.class, sFieldName, JExpr.lit(c.getIRI().toString()));
396: if (configuration.shouldGenerateOwlapiIris()) {
397: voc.field(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, IRI.class,
398: sFieldName.substring(PREFIX_STRING.length()),
399: cm.ref(IRI.class).staticInvoke("create").arg(fv1));
400: }
401: generateJavadoc(o, c, fv1);
402: entities.put(c, voc.staticRef(fv1));
403: }
404: }
405:
406: private void generateOntologyIrisConstants(OWLOntologyManager ontologyManager) {
407: // Get only unique ontology IRIs sorted
408: final List<IRI> ontologyIris = ontologyManager.ontologies().map(o -> o.getOntologyID().getOntologyIRI())
409: .filter(Optional::isPresent).map(Optional::get).distinct()
410: .sorted(Comparator.comparing(IRI::getIRIString))
411: .collect(Collectors.toList());
412: ontologyIris.forEach(iri -> {
413: final String fieldName = ensureUniqueIdentifier("ONTOLOGY_IRI_" + validJavaIDForIRI(iri));
414: voc.field(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, String.class, fieldName, JExpr.lit(iri.toString()));
415: });
416: }
417:
418: private static Optional<String> resolveFieldPrefix(OWLEntity c, Set<IRI> visitedProperties) {
419: if (c.isOWLClass()) {
420: return Optional.of(PREFIX_CLASS);
421: } else if (c.isOWLDatatype()) {
422: return Optional.of(PREFIX_DATATYPE);
423: } else if (c.isOWLDataProperty() || c.isOWLObjectProperty() || c.isOWLAnnotationProperty()) {
424: if (visitedProperties.contains(c.getIRI())) {
425: LOG.debug("Property with IRI {} already processed. Skipping.", c.getIRI());
426: return Optional.empty();
427: }
428: visitedProperties.add(c.getIRI());
429: return Optional.of(PREFIX_PROPERTY);
430: } else if (c.isOWLNamedIndividual()) {
431: return Optional.of(PREFIX_INDIVIDUAL);
432: }
433: return Optional.of("");
434: }
435:
436: private String ensureUniqueIdentifier(String id) {
437: final StringBuilder sb = new StringBuilder(id);
438: while (voc.fields().containsKey(sb.toString())) {
439: sb.append("_A");
440: }
441: return sb.toString();
442: }
443:
444: /**
445: * Generates Javadoc from rdfs:comment annotation (if present).
446: *
447: * @param ontology Ontology from which the model/vocabulary is being generated
448: * @param owlEntity Annotated entity
449: * @param javaElem Element to document with Javadoc
450: * @return Whether the javadoc comment has been generated
451: */
452: private boolean generateJavadoc(OWLOntology ontology, OWLEntity owlEntity, JDocCommentable javaElem) {
453: if (!configuration.shouldGenerateJavadoc()) {
454: return false;
455: }
456: final List<OWLAnnotation> comments = EntitySearcher.getAnnotations(owlEntity, ontology)
457: .filter(a -> a.getProperty().isComment() && a.getValue()
458: .isLiteral())
459: .collect(Collectors.toList());
460: final Optional<OWLAnnotation> langComment = comments.stream().filter(a -> a.getValue().asLiteral()
461: .map(l -> l.hasLang(LANGUAGE))
462: .orElse(false)).findFirst();
463: // First try finding a comment with a matching language tag
464: if (langComment.isPresent()) {
465: langComment.flatMap(a -> a.getValue().asLiteral())
466: .ifPresent(lit -> javaElem.javadoc().add(lit.getLiteral()));
467: return true;
468: }
469: // If there is none such, just use the first available one
470: if (!comments.isEmpty()) {
471: OWLAnnotation anyComment = comments.get(0);
472: anyComment.getValue().asLiteral().ifPresent(lit -> javaElem.javadoc().add(lit.getLiteral()));
473: return true;
474: }
475: return false;
476: }
477:
478: private JDefinedClass create(final String pkg, final JCodeModel cm, final OWLClass clazz,
479: final OWLOntology ontology) {
480: JDefinedClass cls;
481:
482: String name = pkg + PACKAGE_SEPARATOR + javaClassId(ontology, clazz);
483:
484: try {
485: cls = cm._class(name);
486:
487: cls.annotate(cz.cvut.kbss.jopa.model.annotations.OWLClass.class).param("iri", entities.get(clazz));
488: cls._implements(Serializable.class);
489:
490: generateClassJavadoc(ontology, clazz, cls);
491: } catch (JClassAlreadyExistsException e) {
492: LOG.trace("Class already exists. Using the existing version. {}", e.getMessage());
493: cls = cm._getClass(name);
494: }
495: return cls;
496: }
497:
498: /**
499: * Add common properties such as id and type
500: */
501: private void addCommonClassFields(final JCodeModel cm, final JDefinedClass cls,
502: final PropertiesType propertiesType) {
503: // @Id(generated = true) protected String id;
504: final JClass ftId = cm.ref(String.class);
505: final JFieldVar fvId = addField(ID_FIELD_NAME, cls, ftId);
506: JAnnotationUse a = fvId.annotate(Id.class);
507: a.param("generated", true);
508:
509: JFieldVar fvLabel = null;
510: if (configuration.shouldGenerateAnnotationFields()) {
511: // @OWLAnnotationProperty(iri = RDFS.LABEL) String name;
512: final JClass ftLabel = cm.ref(String.class);
513: fvLabel = addField(LABEL_FIELD_NAME, cls, ftLabel);
514: fvLabel.annotate(OWLAnnotationProperty.class).param("iri", cm.ref(RDFS.class).staticRef("LABEL"));
515:
516: // @OWLAnnotationProperty(iri = DC.Terms.DESCRIPTION) String description;
517: final JClass ftDescription = cm.ref(String.class);
518: final JFieldVar fvDescription = addField(DESCRIPTION_FIELD_NAME, cls, ftDescription);
519: fvDescription.annotate(OWLAnnotationProperty.class)
520: .param("iri", cm.ref(DC.Elements.class).staticRef("DESCRIPTION"));
521: }
522:
523: // @Types Set<String> types;
524: final JClass ftTypes = cm.ref(Set.class).narrow(String.class);
525: final JFieldVar fvTypes = addField(TYPES_FIELD_NAME, cls, ftTypes);
526: fvTypes.annotate(Types.class);
527:
528: // @Properties public final Map<String,Set<String>> properties;
529: final Class<?> propertiesTypeC = (propertiesType == PropertiesType.object ? Object.class : String.class);
530: final JClass ftProperties = cm.ref(Map.class)
531: .narrow(cm.ref(String.class), cm.ref(Set.class).narrow(propertiesTypeC));
532: final JFieldVar fvProperties = addField(PROPERTIES_FIELD_NAME, cls, ftProperties);
533: fvProperties.annotate(Properties.class);
534:
535: generateToStringMethod(cls, fvId, fvLabel);
536: }
537:
538: private String javaClassId(OWLOntology ontology, OWLClass owlClass) {
539: final Optional<OWLAnnotation> res = EntitySearcher.getAnnotations(owlClass, ontology)
540: .filter(a -> isJavaClassNameAnnotation(a) &&
541: a.getValue().isLiteral()).findFirst();
542: if (res.isPresent()) {
543: return res.get().getValue().asLiteral().get().getLiteral();
544: } else {
545: return toJavaNotation(validJavaIDForIRI(owlClass.getIRI()));
546: }
547: }
548:
549: private void generateClassJavadoc(OWLOntology ontology, OWLEntity owlEntity, JDocCommentable javaElem) {
550: final boolean generated = generateJavadoc(ontology, owlEntity, javaElem);
551: if (generated) {
552: javaElem.javadoc().add("\n\n");
553: }
554: generateAuthorshipDoc(javaElem);
555: }
556:
557: private static void generateToStringMethod(JDefinedClass cls, JFieldVar idField, JFieldVar labelField) {
558: final JMethod toString = cls.method(JMod.PUBLIC, String.class, "toString");
559: toString.annotate(Override.class);
560: final JBlock body = toString.body();
561: JExpression expression = JExpr.lit(cls.name() + " {");
562: if (labelField != null) {
563: expression = expression.plus(JExpr.ref(labelField.name()));
564: }
565: expression = expression.plus(JExpr.lit("<")).plus(JExpr.ref(idField.name())).plus(JExpr.lit(">"));
566: expression = expression.plus(JExpr.lit("}"));
567:
568: body._return(expression);
569: }
570:
571: private JDefinedClass ensureCreated(final String pkg, final JCodeModel cm, final OWLClass clazz,
572: final OWLOntology ontology) {
573: if (!classes.containsKey(clazz)) {
574: classes.put(clazz, create(pkg, cm, clazz, ontology));
575: }
576: return classes.get(clazz);
577: }
578:
579: private boolean isJavaClassNameAnnotation(OWLAnnotation a) {
580: final String classNameProperty = (String) configuration.getCliParams()
581: .valueOf(Option.JAVA_CLASSNAME_ANNOTATION.arg);
582: return a.getProperty().getIRI()
583: .equals(IRI.create(classNameProperty != null ? classNameProperty : Constants.P_CLASS_NAME));
584: }
585:
586: /**
587: * Converts a class name to the Java camel case notation
588: *
589: * @param className Generated class name
590: * @return Converted class name
591: */
592: private static String toJavaNotation(String className) {
593: StringBuilder result = new StringBuilder();
594: for (String w : className.split("_")) {
595: if (!w.isEmpty()) {
596: result.append(w.substring(0, 1).toUpperCase()).append(w.substring(1));
597: }
598: }
599: return result.toString();
600: }
601: }