1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2002,2008 Oracle.  All rights reserved.
5 *
6 * $Id: Evolver.java,v 1.2 2008/02/08 20:12:37 mark Exp $
7 */
8
9package com.sleepycat.persist.impl;
10
11import java.io.FileNotFoundException;
12import java.util.ArrayList;
13import java.util.List;
14import java.util.HashMap;
15import java.util.HashSet;
16import java.util.IdentityHashMap;
17import java.util.Map;
18import java.util.Set;
19
20import com.sleepycat.compat.DbCompat;
21import com.sleepycat.db.DatabaseException;
22import com.sleepycat.db.Transaction;
23import com.sleepycat.persist.evolve.Converter;
24import com.sleepycat.persist.evolve.Deleter;
25import com.sleepycat.persist.evolve.Mutation;
26import com.sleepycat.persist.evolve.Mutations;
27import com.sleepycat.persist.evolve.Renamer;
28import com.sleepycat.persist.model.SecondaryKeyMetadata;
29
30/**
31 * Evolves each old format that is still relevant if necessary, using Mutations
32 * to configure deleters, renamers, and converters.
33 *
34 * @author Mark Hayes
35 */
36class Evolver {
37
38    static final int EVOLVE_NONE = 0;
39    static final int EVOLVE_NEEDED = 1;
40    static final int EVOLVE_FAILURE = 2;
41
42    private PersistCatalog catalog;
43    private String storePrefix;
44    private Mutations mutations;
45    private Map<String,Format> newFormats;
46    private boolean forceEvolution;
47    private boolean disallowClassChanges;
48    private boolean nestedFormatsChanged;
49    private Map<Format,Format> changedFormats;
50    private StringBuilder errors;
51    private Set<String> deleteDbs;
52    private Map<String,String> renameDbs;
53    private Map<Format,Format> renameFormats;
54    private Map<Integer,Boolean> evolvedFormats;
55    private List<Format> unprocessedFormats;
56    private Map<Format,Set<Format>> subclassMap;
57
58    Evolver(PersistCatalog catalog,
59            String storePrefix,
60            Mutations mutations,
61            Map<String,Format> newFormats,
62            boolean forceEvolution,
63            boolean disallowClassChanges) {
64        this.catalog = catalog;
65        this.storePrefix = storePrefix;
66        this.mutations = mutations;
67        this.newFormats = newFormats;
68        this.forceEvolution = forceEvolution;
69        this.disallowClassChanges = disallowClassChanges;
70        changedFormats = new IdentityHashMap<Format,Format>();
71        errors = new StringBuilder();
72        deleteDbs = new HashSet<String>();
73        renameDbs = new HashMap<String,String>();
74        renameFormats = new IdentityHashMap<Format,Format>();
75        evolvedFormats = new HashMap<Integer,Boolean>();
76        unprocessedFormats = new ArrayList<Format>();
77        subclassMap = catalog.getSubclassMap();
78    }
79
80    final Mutations getMutations() {
81        return mutations;
82    }
83
84    /**
85     * Returns whether any formats were changed during evolution, and therefore
86     * need to be stored in the catalog.
87     */
88    boolean areFormatsChanged() {
89        return !changedFormats.isEmpty();
90    }
91
92    /**
93     * Returns whether the given format was changed during evolution.
94     */
95    boolean isFormatChanged(Format format) {
96        return changedFormats.containsKey(format);
97    }
98
99    private void setFormatsChanged(Format oldFormat) {
100        checkClassChangesAllowed(oldFormat);
101        changedFormats.put(oldFormat, oldFormat);
102        nestedFormatsChanged = true;
103        /* PersistCatalog.expectNoClassChanges is true in unit tests only. */
104        if (PersistCatalog.expectNoClassChanges) {
105            throw new IllegalStateException("expectNoClassChanges");
106        }
107    }
108
109    private void checkClassChangesAllowed(Format oldFormat) {
110        if (disallowClassChanges) {
111            throw new IllegalStateException
112                ("When performing an upgrade changes are not allowed " +
113                 "but were made to: " + oldFormat.getClassName());
114        }
115    }
116
117    /**
118     * Returns the set of formats for a specific superclass format, or null if
119     * the superclass is not a complex type or has not subclasses.
120     */
121    Set<Format> getSubclassFormats(Format superFormat) {
122        return subclassMap.get(superFormat);
123    }
124
125    /**
126     * Returns an error string if any mutations are invalid or missing, or
127     * returns null otherwise.  If non-null is returned, the store may not be
128     * opened.
129     */
130    String getErrors() {
131        if (errors.length() > 0) {
132            return errors.toString();
133        } else {
134            return null;
135        }
136    }
137
138    /**
139     * Adds a newline and the given error.
140     */
141    private void addError(String error) {
142        if (errors.length() > 0) {
143            errors.append("\n---\n");
144        }
145        errors.append(error);
146    }
147
148    private String getClassVersionLabel(Format format, String prefix) {
149        if (format != null) {
150            return prefix +
151                   " class: " + format.getClassName() +
152                   " version: " + format.getVersion();
153        } else {
154            return "";
155        }
156    }
157
158    /**
159     * Adds a specified error when no specific mutation is involved.
160     */
161    void addEvolveError(Format oldFormat,
162                        Format newFormat,
163                        String scenario,
164                        String error) {
165        checkClassChangesAllowed(oldFormat);
166        if (scenario == null) {
167            scenario = "Error";
168        }
169        addError(scenario + " when evolving" +
170                 getClassVersionLabel(oldFormat, "") +
171                 getClassVersionLabel(newFormat, " to") +
172                 " Error: " + error);
173    }
174
175    /**
176     * Adds an error for an invalid mutation.
177     */
178    void addInvalidMutation(Format oldFormat,
179                            Format newFormat,
180                            Mutation mutation,
181                            String error) {
182        checkClassChangesAllowed(oldFormat);
183        addError("Invalid mutation: " + mutation +
184                 getClassVersionLabel(oldFormat, " For") +
185                 getClassVersionLabel(newFormat, " New") +
186                 " Error: " + error);
187    }
188
189    /**
190     * Adds an error for a missing mutation.
191     */
192    void addMissingMutation(Format oldFormat,
193                            Format newFormat,
194                            String error) {
195        checkClassChangesAllowed(oldFormat);
196        addError("Mutation is missing to evolve" +
197                 getClassVersionLabel(oldFormat, "") +
198                 getClassVersionLabel(newFormat, " to") +
199                 " Error: " + error);
200    }
201
202    /**
203     * Called by PersistCatalog for all non-entity formats.
204     */
205    void addNonEntityFormat(Format oldFormat) {
206        unprocessedFormats.add(oldFormat);
207    }
208
209    /**
210     * Called by PersistCatalog after calling evolveFormat or
211     * addNonEntityFormat for all old formats.
212     *
213     * We do not require deletion of an unreferenced class for three
214     * reasons: 1) built-in proxy classes may not be referenced, 2) the
215     * user may wish to declare persistent classes that are not yet used.
216     */
217    void finishEvolution() {
218        /* Process unreferenced classes. */
219        for (Format oldFormat : unprocessedFormats) {
220            oldFormat.setUnused(true);
221            Integer oldFormatId = oldFormat.getId();
222            if (!evolvedFormats.containsKey(oldFormatId)) {
223                evolvedFormats.put(oldFormatId, true);
224                boolean result = evolveFormatInternal(oldFormat);
225                evolvedFormats.put(oldFormatId, result);
226            }
227        }
228    }
229
230    /**
231     * Called by PersistCatalog for all entity formats, and by Format.evolve
232     * methods for all potentially referenced non-entity formats.
233     */
234    boolean evolveFormat(Format oldFormat) {
235        boolean result;
236        boolean trackEntityChanges =
237            oldFormat.getLatestVersion().getEntityFormat() != null;
238        boolean saveNestedFormatsChanged = nestedFormatsChanged;
239        if (trackEntityChanges) {
240            nestedFormatsChanged = false;
241        }
242        Integer oldFormatId = oldFormat.getId();
243        if (evolvedFormats.containsKey(oldFormatId)) {
244            result = evolvedFormats.get(oldFormatId);
245        } else {
246            evolvedFormats.put(oldFormatId, true);
247            result = evolveFormatInternal(oldFormat);
248            evolvedFormats.put(oldFormatId, result);
249        }
250        if (oldFormat.getLatestVersion().isNew()) {
251            nestedFormatsChanged = true;
252        }
253        if (trackEntityChanges) {
254            if (nestedFormatsChanged) {
255                Format latest = oldFormat.getLatestVersion().getEntityFormat();
256                if (latest != null) {
257                    latest.setEvolveNeeded(true);
258                }
259            }
260            nestedFormatsChanged = saveNestedFormatsChanged;
261        }
262        return result;
263    }
264
265    /**
266     * Tries to evolve a given existing format to the current version of the
267     * class and returns false if an invalid mutation is encountered or the
268     * configured mutations are not sufficient.
269     */
270    private boolean evolveFormatInternal(Format oldFormat) {
271
272        /* Predefined formats and deleted classes never need evolving. */
273        if (Format.isPredefined(oldFormat) || oldFormat.isDeleted()) {
274            return true;
275        }
276
277        /* Get class mutations. */
278        String oldName = oldFormat.getClassName();
279        int oldVersion = oldFormat.getVersion();
280        Renamer renamer = mutations.getRenamer(oldName, oldVersion, null);
281        Deleter deleter = mutations.getDeleter(oldName, oldVersion, null);
282        Converter converter =
283            mutations.getConverter(oldName, oldVersion, null);
284        if (deleter != null && (converter != null || renamer != null)) {
285            addInvalidMutation
286                (oldFormat, null, deleter,
287                 "Class Deleter not allowed along with a Renamer or " +
288                 "Converter for the same class");
289            return false;
290        }
291
292        /*
293         * For determining the new name, arrays get special treatment.  The
294         * component format is evolved in the process, and we disallow muations
295         * for arrays.
296         */
297        String newName;
298        if (oldFormat.isArray()) {
299            if (deleter != null || converter != null || renamer != null) {
300                Mutation mutation = (deleter != null) ? deleter :
301                    ((converter != null) ? converter : renamer);
302                addInvalidMutation
303                    (oldFormat, null, mutation,
304                     "Mutations not allowed for an array");
305                return false;
306            }
307            Format compFormat = oldFormat.getComponentType();
308            if (!evolveFormat(compFormat)) {
309                return false;
310            }
311            Format latest = compFormat.getLatestVersion();
312            if (latest != compFormat) {
313                newName = (latest.isArray() ? "[" : "[L") +
314                           latest.getClassName() + ';';
315            } else {
316                newName = oldName;
317            }
318        } else {
319            newName = (renamer != null) ? renamer.getNewName() : oldName;
320        }
321
322        /* Try to get the new class format.  Save exception for later. */
323        Format newFormat;
324        String newFormatException;
325        try {
326            Class newClass = SimpleCatalog.classForName(newName);
327            try {
328                newFormat = catalog.createFormat(newClass, newFormats);
329                assert newFormat != oldFormat : newFormat.getClassName();
330                newFormatException = null;
331            } catch (Exception e) {
332                newFormat = null;
333                newFormatException = e.toString();
334            }
335        } catch (ClassNotFoundException e) {
336            newFormat = null;
337            newFormatException = e.toString();
338        }
339
340        if (newFormat != null) {
341
342            /*
343             * If the old format is not the existing latest version and the new
344             * format is not an existing format, then we must evolve the latest
345             * old version to the new format first.  We cannot evolve old
346             * format to a new format that may be discarded because it is equal
347             * to the latest existing format (which will remain the current
348             * version).
349             */
350            if (oldFormat != oldFormat.getLatestVersion() &&
351                newFormat.getPreviousVersion() == null) {
352                assert newFormats.containsValue(newFormat);
353                Format oldLatestFormat = oldFormat.getLatestVersion();
354                evolveFormat(oldLatestFormat);
355                if (oldLatestFormat == oldLatestFormat.getLatestVersion()) {
356                    assert !newFormats.containsValue(newFormat);
357                    /* newFormat equals oldLatestFormat and was discarded. */
358                    newFormat = oldLatestFormat;
359                }
360            }
361
362            /*
363             * If the old format was previously evolved to the new format
364             * (which means the new format is actually an existing format),
365             * then there is nothing to do.  This is the case where no class
366             * changes were made.
367             *
368             * However, if mutations were specified when opening the catalog
369             * that are different than the mutations last used, then we must
370             * force the re-evolution of all old formats.
371             */
372            if (!forceEvolution &&
373                newFormat == oldFormat.getLatestVersion()) {
374                return true;
375            }
376        }
377
378        /* Apply class Renamer and continue if successful. */
379        if (renamer != null) {
380            if (!applyRenamer(renamer, oldFormat, newFormat)) {
381                return false;
382            }
383        }
384
385        /* Apply class Converter and return. */
386        if (converter != null) {
387            if (oldFormat.isEntity()) {
388                if (newFormat == null || !newFormat.isEntity()) {
389                    addInvalidMutation
390                        (oldFormat, newFormat, deleter,
391                         "Class converter not allowed for an entity class " +
392                         "that is no longer present or not having an " +
393                         "@Entity annotation");
394                    return false;
395                }
396                if (!oldFormat.evolveMetadata(newFormat, converter, this)) {
397                    return false;
398                }
399            }
400            return applyConverter(converter, oldFormat, newFormat);
401        }
402
403        /* Apply class Deleter and return. */
404        boolean needDeleter =
405            (newFormat == null) ||
406            (newFormat.isEntity() != oldFormat.isEntity());
407        if (deleter != null) {
408            if (!needDeleter) {
409                addInvalidMutation
410                    (oldFormat, newFormat, deleter,
411                     "Class deleter not allowed when the class and its " +
412                     "@Entity or @Persistent annotation is still present");
413                return false;
414            }
415            return applyDeleter(deleter, oldFormat, newFormat);
416        } else {
417            if (needDeleter) {
418                if (newFormat == null) {
419                    assert newFormatException != null;
420		    /* FindBugs newFormat known to be null excluded. */
421                    addMissingMutation
422                        (oldFormat, newFormat, newFormatException);
423                } else {
424                    addMissingMutation
425                        (oldFormat, newFormat,
426                         "@Entity switched to/from @Persistent");
427                }
428                return false;
429            }
430        }
431
432        /*
433         * Class-level mutations have been applied.  Now apply field mutations
434         * (for complex classes) or special conversions (enum conversions, for
435         * example) by calling the old format's evolve method.
436         */
437        return oldFormat.evolve(newFormat, this);
438    }
439
440    /**
441     * Use the old format and discard the new format.  Called by
442     * Format.evolve when the old and new formats are identical.
443     */
444    void useOldFormat(Format oldFormat, Format newFormat) {
445        Format renamedFormat = renameFormats.get(oldFormat);
446        if (renamedFormat != null) {
447
448            /*
449             * The format was renamed but, because this method is called, we
450             * know that no other class changes were made.  Use the new/renamed
451             * format as the reader.
452             */
453            assert renamedFormat == newFormat;
454            useEvolvedFormat(oldFormat, renamedFormat, renamedFormat);
455        } else if (newFormat != null &&
456                   oldFormat.getVersion() != newFormat.getVersion()) {
457            /* The user wants a new version number, but no other changes. */
458            useEvolvedFormat(oldFormat, newFormat, newFormat);
459        } else {
460            /* The new format is discarded. */
461            catalog.useExistingFormat(oldFormat);
462            if (newFormat != null) {
463                newFormats.remove(newFormat.getClassName());
464            }
465        }
466    }
467
468    /**
469     * Install an evolver Reader in the old format.  Called by Format.evolve
470     * when the old and new formats are not identical.
471     */
472    void useEvolvedFormat(Format oldFormat,
473                          Reader evolveReader,
474                          Format newFormat) {
475        oldFormat.setReader(evolveReader);
476        if (newFormat != null) {
477            oldFormat.setLatestVersion(newFormat);
478        }
479        setFormatsChanged(oldFormat);
480    }
481
482    private boolean applyRenamer(Renamer renamer,
483                                 Format oldFormat,
484                                 Format newFormat) {
485        if (!checkUpdatedVersion(renamer, oldFormat, newFormat)) {
486            return false;
487        }
488        if (oldFormat.isEntity() && oldFormat.isCurrentVersion()) {
489            String newClassName = newFormat.getClassName();
490            String oldClassName = oldFormat.getClassName();
491            /* Queue the renaming of the primary and secondary databases. */
492            renameDbs.put
493                (Store.makePriDbName(storePrefix, oldClassName),
494                 Store.makePriDbName(storePrefix, newClassName));
495            for (SecondaryKeyMetadata keyMeta :
496                 oldFormat.getEntityMetadata().getSecondaryKeys().values()) {
497                String keyName = keyMeta.getKeyName();
498                renameDbs.put
499                    (Store.makeSecDbName(storePrefix, oldClassName, keyName),
500                     Store.makeSecDbName(storePrefix, newClassName, keyName));
501            }
502        }
503
504        /*
505         * Link the old format to the renamed format so that we can detect the
506         * rename in useOldFormat.
507         */
508        renameFormats.put(oldFormat, newFormat);
509
510        setFormatsChanged(oldFormat);
511        return true;
512    }
513
514    /**
515     * Called by ComplexFormat when a secondary key name is changed.
516     */
517    void renameSecondaryDatabase(Format oldFormat,
518                                 Format newFormat,
519                                 String oldKeyName,
520                                 String newKeyName) {
521        renameDbs.put
522            (Store.makeSecDbName
523                (storePrefix, oldFormat.getClassName(), oldKeyName),
524             Store.makeSecDbName
525                (storePrefix, newFormat.getClassName(), newKeyName));
526    }
527
528    private boolean applyDeleter(Deleter deleter,
529                                 Format oldFormat,
530                                 Format newFormat) {
531        if (!checkUpdatedVersion(deleter, oldFormat, newFormat)) {
532            return false;
533        }
534        if (oldFormat.isEntity() && oldFormat.isCurrentVersion()) {
535            /* Queue the deletion of the primary and secondary databases. */
536            String className = oldFormat.getClassName();
537            deleteDbs.add(Store.makePriDbName(storePrefix, className));
538            for (SecondaryKeyMetadata keyMeta :
539                 oldFormat.getEntityMetadata().getSecondaryKeys().values()) {
540                deleteDbs.add(Store.makeSecDbName
541                    (storePrefix, className, keyMeta.getKeyName()));
542            }
543        }
544
545        /*
546         * Set the format to deleted last,  so that the above test using
547         * isCurrentVersion works properly.
548         */
549        oldFormat.setDeleted(true);
550        if (newFormat != null) {
551            oldFormat.setLatestVersion(newFormat);
552        }
553
554        setFormatsChanged(oldFormat);
555        return true;
556    }
557
558    /**
559     * Called by ComplexFormat when a secondary key is dropped.
560     */
561    void deleteSecondaryDatabase(Format oldFormat, String keyName) {
562        deleteDbs.add(Store.makeSecDbName
563            (storePrefix, oldFormat.getClassName(), keyName));
564    }
565
566    private boolean applyConverter(Converter converter,
567                                   Format oldFormat,
568                                   Format newFormat) {
569        if (!checkUpdatedVersion(converter, oldFormat, newFormat)) {
570            return false;
571        }
572        Reader reader = new ConverterReader(converter);
573        useEvolvedFormat(oldFormat, reader, newFormat);
574        return true;
575    }
576
577    boolean isClassConverted(Format format) {
578        return format.getReader() instanceof ConverterReader;
579    }
580
581    private boolean checkUpdatedVersion(Mutation mutation,
582                                        Format oldFormat,
583                                        Format newFormat) {
584        if (newFormat != null &&
585            !oldFormat.isEnum() &&
586            newFormat.getVersion() <= oldFormat.getVersion()) {
587            addInvalidMutation
588                (oldFormat, newFormat, mutation,
589                 "A new higher version number must be assigned");
590            return false;
591        } else {
592            return true;
593        }
594    }
595
596    boolean checkUpdatedVersion(String scenario,
597                                Format oldFormat,
598                                Format newFormat) {
599        if (newFormat != null &&
600            !oldFormat.isEnum() &&
601            newFormat.getVersion() <= oldFormat.getVersion()) {
602            addEvolveError
603                (oldFormat, newFormat, scenario,
604                 "A new higher version number must be assigned");
605            return false;
606        } else {
607            return true;
608        }
609    }
610
611    void renameAndRemoveDatabases(Store store, Transaction txn)
612        throws DatabaseException {
613
614        for (String dbName : deleteDbs) {
615            try {
616                String[] fileAndDbNames = store.parseDbName(dbName);
617                DbCompat.removeDatabase
618                    (store.getEnvironment(), txn,
619                     fileAndDbNames[0], fileAndDbNames[1]);
620            } catch (FileNotFoundException ignored) {
621            }
622        }
623        for (Map.Entry<String,String> entry : renameDbs.entrySet()) {
624            String oldName = entry.getKey();
625            String newName = entry.getValue();
626            try {
627                String[] oldFileAndDbNames = store.parseDbName(oldName);
628                String[] newFileAndDbNames = store.parseDbName(newName);
629                DbCompat.renameDatabase
630                    (store.getEnvironment(), txn,
631                     oldFileAndDbNames[0], oldFileAndDbNames[1],
632                     newFileAndDbNames[0], newFileAndDbNames[1]);
633            } catch (FileNotFoundException ignored) {
634            }
635        }
636    }
637
638    /**
639     * Evolves a primary key field or composite key field.
640     */
641    int evolveRequiredKeyField(Format oldParent,
642                               Format newParent,
643                               FieldInfo oldField,
644                               FieldInfo newField) {
645        int result = EVOLVE_NONE;
646        String oldName = oldField.getName();
647        final String FIELD_KIND =
648            "primary key field or composite key class field";
649        final String FIELD_LABEL =
650            FIELD_KIND + ": " + oldName;
651
652        if (newField == null) {
653            addMissingMutation
654                (oldParent, newParent,
655                 "Field is missing and deletion is not allowed for a " +
656                 FIELD_LABEL);
657            return EVOLVE_FAILURE;
658        }
659
660        /* Check field mutations.  Only a Renamer is allowed. */
661        Deleter deleter = mutations.getDeleter
662            (oldParent.getClassName(), oldParent.getVersion(), oldName);
663        if (deleter != null) {
664            addInvalidMutation
665                (oldParent, newParent, deleter,
666                 "Deleter is not allowed for a " + FIELD_LABEL);
667            return EVOLVE_FAILURE;
668        }
669        Converter converter = mutations.getConverter
670            (oldParent.getClassName(), oldParent.getVersion(), oldName);
671        if (converter != null) {
672            addInvalidMutation
673                (oldParent, newParent, converter,
674                 "Converter is not allowed for a " + FIELD_LABEL);
675            return EVOLVE_FAILURE;
676        }
677        Renamer renamer = mutations.getRenamer
678            (oldParent.getClassName(), oldParent.getVersion(), oldName);
679        String newName = newField.getName();
680        if (renamer != null) {
681            if (!renamer.getNewName().equals(newName)) {
682                addInvalidMutation
683                    (oldParent, newParent, converter,
684                     "Converter is not allowed for a " + FIELD_LABEL);
685                return EVOLVE_FAILURE;
686            }
687            result = EVOLVE_NEEDED;
688        } else {
689            if (!oldName.equals(newName)) {
690                addMissingMutation
691                    (oldParent, newParent,
692                     "Renamer is required when field name is changed from: " +
693                     oldName + " to: " + newName);
694                return EVOLVE_FAILURE;
695            }
696        }
697
698        /*
699         * Evolve the declared version of the field format.
700         */
701        Format oldFieldFormat = oldField.getType();
702        if (!evolveFormat(oldFieldFormat)) {
703            return EVOLVE_FAILURE;
704        }
705        Format oldLatestFormat = oldFieldFormat.getLatestVersion();
706        Format newFieldFormat = newField.getType();
707
708        if (oldLatestFormat.getClassName().equals
709                (newFieldFormat.getClassName())) {
710            /* Formats are identical. */
711            return result;
712        } else if ((oldLatestFormat.getWrapperFormat() != null &&
713                    oldLatestFormat.getWrapperFormat().getId() ==
714                    newFieldFormat.getId()) ||
715                   (newFieldFormat.getWrapperFormat() != null &&
716                    newFieldFormat.getWrapperFormat().getId() ==
717                    oldLatestFormat.getId())) {
718            /* Primitive <-> primitive wrapper type change. */
719            return EVOLVE_NEEDED;
720        } else {
721            /* Type was changed incompatibly. */
722            addEvolveError
723                (oldParent, newParent,
724                 "Type may not be changed for a " + FIELD_KIND,
725                 "Old field type: " + oldLatestFormat.getClassName() +
726                 " is not compatible with the new type: " +
727                 newFieldFormat.getClassName() +
728                 " for field: " + oldName);
729            return EVOLVE_FAILURE;
730        }
731    }
732}
733