001package com.box.sdk;
002
003import java.net.URL;
004import java.util.ArrayList;
005import java.util.List;
006
007import com.eclipsesource.json.JsonArray;
008import com.eclipsesource.json.JsonObject;
009import com.eclipsesource.json.JsonValue;
010
011
012/**
013 * The MetadataTemplate class represents the Box metadata template object.
014 * Templates allow the metadata service to provide a multitude of services,
015 * such as pre-defining sets of key:value pairs or schema enforcement on specific fields.
016 *
017 * @see <a href="https://docs.box.com/reference#metadata-templates">Box metadata templates</a>
018 */
019public class MetadataTemplate extends BoxJSONObject {
020
021    /**
022     * @see #getMetadataTemplate(BoxAPIConnection)
023     */
024    public static final URLTemplate METADATA_TEMPLATE_URL_TEMPLATE
025        = new URLTemplate("metadata_templates/%s/%s/schema");
026
027    /**
028     * @see #createMetadataTemplate(BoxAPIConnection, String, String, String, boolean, List)
029     */
030    public static final URLTemplate METADATA_TEMPLATE_SCHEMA_URL_TEMPLATE
031        = new URLTemplate("metadata_templates/schema");
032
033    /**
034     * @see #getEnterpriseMetadataTemplates(String, int, BoxAPIConnection, String...)
035     */
036    public static final URLTemplate ENTERPRISE_METADATA_URL_TEMPLATE = new URLTemplate("metadata_templates/%s");
037
038    /**
039     * Default metadata type to be used in query.
040     */
041    private static final String DEFAULT_METADATA_TYPE = "properties";
042
043    /**
044     * Global metadata scope. Used by default if the metadata type is "properties".
045     */
046    private static final String GLOBAL_METADATA_SCOPE = "global";
047
048    /**
049     * Enterprise metadata scope. Used by default if the metadata type is not "properties".
050     */
051    private static final String ENTERPRISE_METADATA_SCOPE = "enterprise";
052
053    /**
054     * Default number of entries per page.
055     */
056    private static final int DEFAULT_ENTRIES_LIMIT = 100;
057
058    /**
059     * @see #getTemplateKey()
060     */
061    private String templateKey;
062
063    /**
064     * @see #getScope()
065     */
066    private String scope;
067
068    /**
069     * @see #getDisplayName()
070     */
071    private String displayName;
072
073    /**
074     * @see #getIsHidden()
075     */
076    private Boolean isHidden;
077
078    /**
079     * @see #getFields()
080     */
081    private List<Field> fields;
082
083    /**
084     * Constructs an empty metadata template.
085     */
086    public MetadataTemplate() {
087        super();
088    }
089
090    /**
091     * Constructs a metadata template from a JSON string.
092     * @param json the json encoded metadate template.
093     */
094    public MetadataTemplate(String json) {
095        super(json);
096    }
097
098    /**
099     * Constructs a metadate template from a JSON object.
100     * @param jsonObject the json encoded metadate template.
101     */
102    MetadataTemplate(JsonObject jsonObject) {
103        super(jsonObject);
104    }
105
106    /**
107     * Gets the unique template key to identify the metadata template.
108     * @return the unique template key to identify the metadata template.
109     */
110    public String getTemplateKey() {
111        return this.templateKey;
112    }
113
114    /**
115     * Gets the metadata template scope.
116     * @return the metadata template scope.
117     */
118    public String getScope() {
119        return this.scope;
120    }
121
122    /**
123     * Gets the displayed metadata template name.
124     * @return the displayed metadata template name.
125     */
126    public String getDisplayName() {
127        return this.displayName;
128    }
129
130    /**
131     * Gets is the metadata template hidden.
132     * @return is the metadata template hidden.
133     */
134    public Boolean getIsHidden() {
135        return this.isHidden;
136    }
137
138    /**
139     * Gets the iterable with all fields the metadata template contains.
140     * @return the iterable with all fields the metadata template contains.
141     */
142    public List<Field> getFields() {
143        return this.fields;
144    }
145
146    /**
147     * {@inheritDoc}
148     */
149    @Override
150    void parseJSONMember(JsonObject.Member member) {
151        JsonValue value = member.getValue();
152        String memberName = member.getName();
153        if (memberName.equals("templateKey")) {
154            this.templateKey = value.asString();
155        } else if (memberName.equals("scope")) {
156            this.scope = value.asString();
157        } else if (memberName.equals("displayName")) {
158            this.displayName = value.asString();
159        } else if (memberName.equals("hidden")) {
160            this.isHidden = value.asBoolean();
161        } else if (memberName.equals("fields")) {
162            this.fields = new ArrayList<Field>();
163            for (JsonValue field: value.asArray()) {
164                this.fields.add(new Field(field.asObject()));
165            }
166        }
167    }
168
169    /**
170     * Creates new metadata template.
171     * @param api the API connection to be used.
172     * @param scope the scope of the object.
173     * @param templateKey a unique identifier for the template.
174     * @param displayName the display name of the field.
175     * @param hidden whether this template is hidden in the UI.
176     * @param fields the ordered set of fields for the template
177     * @return the metadata template returned from the server.
178     */
179    public static MetadataTemplate createMetadataTemplate(BoxAPIConnection api, String scope, String templateKey,
180            String displayName, boolean hidden, List<Field> fields) {
181
182        JsonObject jsonObject = new JsonObject();
183        jsonObject.add("scope", scope);
184        jsonObject.add("displayName", displayName);
185        jsonObject.add("hidden", hidden);
186
187        if (templateKey != null) {
188            jsonObject.add("templateKey", templateKey);
189        }
190
191        JsonArray fieldsArray = new JsonArray();
192        if (fields != null && !fields.isEmpty()) {
193            for (Field field : fields) {
194                JsonObject fieldObj = getFieldJsonObject(field);
195
196                fieldsArray.add(fieldObj);
197            }
198
199            jsonObject.add("fields", fieldsArray);
200        }
201
202        URL url = METADATA_TEMPLATE_SCHEMA_URL_TEMPLATE.build(api.getBaseURL());
203        BoxJSONRequest request = new BoxJSONRequest(api, url, "POST");
204        request.setBody(jsonObject.toString());
205
206        BoxJSONResponse response = (BoxJSONResponse) request.send();
207        JsonObject responseJSON = JsonObject.readFrom(response.getJSON());
208
209        return new MetadataTemplate(responseJSON);
210    }
211
212    /**
213     * Gets the JsonObject representation of the given field object.
214     * @param field represents a template field
215     * @return the json object
216     */
217    private static JsonObject getFieldJsonObject(Field field) {
218        JsonObject fieldObj = new JsonObject();
219        fieldObj.add("type", field.getType());
220        fieldObj.add("key", field.getKey());
221        fieldObj.add("displayName", field.getDisplayName());
222
223        String fieldDesc = field.getDescription();
224        if (fieldDesc != null) {
225            fieldObj.add("description", field.getDescription());
226        }
227
228        Boolean fieldIsHidden = field.getIsHidden();
229        if (fieldIsHidden != null) {
230            fieldObj.add("hidden", field.getIsHidden());
231        }
232
233        JsonArray array = new JsonArray();
234        List<String> options = field.getOptions();
235        if (options != null && !options.isEmpty()) {
236            for (String option : options) {
237                JsonObject optionObj = new JsonObject();
238                optionObj.add("key", option);
239
240                array.add(optionObj);
241            }
242            fieldObj.add("options", array);
243        }
244
245        return fieldObj;
246    }
247
248    /**
249     * Updates the schema of an existing metadata template.
250     *
251     * @param api the API connection to be used
252     * @param scope the scope of the object
253     * @param template Unique identifier of the template
254     * @param fieldOperations the fields that needs to be updated / added in the template
255     * @return the updated metadata template
256     */
257    public static MetadataTemplate updateMetadataTemplate(BoxAPIConnection api, String scope, String template,
258            List<FieldOperation> fieldOperations) {
259
260        JsonArray array = new JsonArray();
261
262        for (FieldOperation fieldOperation : fieldOperations) {
263            JsonObject jsonObject = getFieldOperationJsonObject(fieldOperation);
264            array.add(jsonObject);
265        }
266
267        QueryStringBuilder builder = new QueryStringBuilder();
268        URL url = METADATA_TEMPLATE_URL_TEMPLATE.build(api.getBaseURL(), scope, template);
269        BoxJSONRequest request = new BoxJSONRequest(api, url, "PUT");
270        request.setBody(array.toString());
271
272        BoxJSONResponse response = (BoxJSONResponse) request.send();
273        JsonObject responseJson = JsonObject.readFrom(response.getJSON());
274
275        return new MetadataTemplate(responseJson);
276    }
277
278    /**
279     * Gets the JsonObject representation of the Field Operation.
280     * @param fieldOperation represents the template update operation
281     * @return the json object
282     */
283    private static JsonObject getFieldOperationJsonObject(FieldOperation fieldOperation) {
284        JsonObject jsonObject = new JsonObject();
285        jsonObject.add("op", fieldOperation.getOp().toString());
286
287        String fieldKey = fieldOperation.getFieldKey();
288        if (fieldKey != null) {
289            jsonObject.add("fieldKey", fieldKey);
290        }
291
292        Field field = fieldOperation.getData();
293        if (field != null) {
294            JsonObject fieldObj = new JsonObject();
295
296            String type = field.getType();
297            if (type != null) {
298                fieldObj.add("type", type);
299            }
300
301            String key = field.getKey();
302            if (key != null) {
303                fieldObj.add("key", key);
304            }
305
306            String displayName = field.getDisplayName();
307            if (displayName != null) {
308                fieldObj.add("displayName", displayName);
309            }
310
311            String description = field.getDescription();
312            if (description != null) {
313                fieldObj.add("description", description);
314            }
315
316            Boolean hidden = field.getIsHidden();
317            if (hidden != null) {
318                fieldObj.add("hidden", hidden);
319            }
320
321            List<String> options = field.getOptions();
322            if (options != null) {
323                JsonArray array = new JsonArray();
324                for (String option: options) {
325                    JsonObject optionObj = new JsonObject();
326                    optionObj.add("key", option);
327
328                    array.add(optionObj);
329                }
330
331                fieldObj.add("options", array);
332            }
333
334            jsonObject.add("data", fieldObj);
335        }
336
337        List<String> fieldKeys = fieldOperation.getFieldKeys();
338        if (fieldKeys != null) {
339            jsonObject.add("fieldKeys", getJsonArray(fieldKeys));
340        }
341
342        List<String> enumOptionKeys = fieldOperation.getEnumOptionKeys();
343        if (enumOptionKeys != null) {
344            jsonObject.add("enumOptionKeys", getJsonArray(enumOptionKeys));
345        }
346
347        String enumOptionKey = fieldOperation.getEnumOptionKey();
348        if (enumOptionKey != null) {
349            jsonObject.add("enumOptionKey", enumOptionKey);
350        }
351        return jsonObject;
352    }
353
354    /**
355     * Gets the Json Array representation of the given list of strings.
356     * @param keys List of strings
357     * @return the JsonArray represents the list of keys
358     */
359    private static JsonArray getJsonArray(List<String> keys) {
360        JsonArray array = new JsonArray();
361        for (String key : keys) {
362            array.add(key);
363        }
364
365        return array;
366    }
367
368    /**
369     * Gets the metadata template of properties.
370     * @param api the API connection to be used.
371     * @return the metadata template returned from the server.
372     */
373    public static MetadataTemplate getMetadataTemplate(BoxAPIConnection api) {
374        return getMetadataTemplate(api, DEFAULT_METADATA_TYPE);
375    }
376
377    /**
378     * Gets the metadata template of specified template type.
379     * @param api the API connection to be used.
380     * @param templateName the metadata template type name.
381     * @return the metadata template returned from the server.
382     */
383    public static MetadataTemplate getMetadataTemplate(BoxAPIConnection api, String templateName) {
384        String scope = scopeBasedOnType(templateName);
385        return getMetadataTemplate(api, templateName, scope);
386    }
387
388    /**
389     * Gets the metadata template of specified template type.
390     * @param api the API connection to be used.
391     * @param templateName the metadata template type name.
392     * @param scope the metadata template scope (global or enterprise).
393     * @param fields the fields to retrieve.
394     * @return the metadata template returned from the server.
395     */
396    public static MetadataTemplate getMetadataTemplate(
397            BoxAPIConnection api, String templateName, String scope, String ... fields) {
398        QueryStringBuilder builder = new QueryStringBuilder();
399        if (fields.length > 0) {
400            builder.appendParam("fields", fields);
401        }
402        URL url = METADATA_TEMPLATE_URL_TEMPLATE.buildWithQuery(
403                api.getBaseURL(), builder.toString(), scope, templateName);
404        BoxAPIRequest request = new BoxAPIRequest(api, url, "GET");
405        BoxJSONResponse response = (BoxJSONResponse) request.send();
406        return new MetadataTemplate(response.getJSON());
407    }
408
409    /**
410     * Returns all metadata templates within a user's enterprise.
411     * @param api the API connection to be used.
412     * @param fields the fields to retrieve.
413     * @return the metadata template returned from the server.
414     */
415    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(BoxAPIConnection api, String ... fields) {
416        return getEnterpriseMetadataTemplates(ENTERPRISE_METADATA_SCOPE, api, fields);
417    }
418
419    /**
420     * Returns all metadata templates within a user's scope. Currently only the enterprise scope is supported.
421     * @param scope the scope of the metadata templates.
422     * @param api the API connection to be used.
423     * @param fields the fields to retrieve.
424     * @return the metadata template returned from the server.
425     */
426    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(
427            String scope, BoxAPIConnection api, String ... fields) {
428        return getEnterpriseMetadataTemplates(ENTERPRISE_METADATA_SCOPE, DEFAULT_ENTRIES_LIMIT, api, fields);
429    }
430
431    /**
432     * Returns all metadata templates within a user's scope. Currently only the enterprise scope is supported.
433     * @param scope the scope of the metadata templates.
434     * @param limit maximum number of entries per response.
435     * @param api the API connection to be used.
436     * @param fields the fields to retrieve.
437     * @return the metadata template returned from the server.
438     */
439    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(
440            String scope, int limit, BoxAPIConnection api, String ... fields) {
441        QueryStringBuilder builder = new QueryStringBuilder();
442        if (fields.length > 0) {
443            builder.appendParam("fields", fields);
444        }
445        return new BoxResourceIterable<MetadataTemplate>(
446                api, ENTERPRISE_METADATA_URL_TEMPLATE.buildWithQuery(
447                        api.getBaseURL(), builder.toString(), scope), limit) {
448
449            @Override
450            protected MetadataTemplate factory(JsonObject jsonObject) {
451                return new MetadataTemplate(jsonObject);
452            }
453        };
454    }
455
456    /**
457     * Determines the metadata scope based on type.
458     * @param typeName type of the metadata.
459     * @return scope of the metadata.
460     */
461    private static String scopeBasedOnType(String typeName) {
462        return typeName.equals(DEFAULT_METADATA_TYPE) ? GLOBAL_METADATA_SCOPE : ENTERPRISE_METADATA_SCOPE;
463    }
464
465    /**
466     * Class contains information about the metadata template field.
467     */
468    public static class Field extends BoxJSONObject {
469
470        /**
471         * @see #getType()
472         */
473        private String type;
474
475        /**
476         * @see #getKey()
477         */
478        private String key;
479
480        /**
481         * @see #getDisplayName()
482         */
483        private String displayName;
484
485        /**
486         * @see #getIsHidden()
487         */
488        private Boolean isHidden;
489
490        /**
491         * @see #getDescription()
492         */
493        private String description;
494
495        /**
496         * @see #getOptions()
497         */
498        private List<String> options;
499
500        /**
501         * Constructs an empty metadata template.
502         */
503        public Field() {
504            super();
505        }
506
507        /**
508         * Constructs a metadate template field from a JSON string.
509         * @param json the json encoded metadate template field.
510         */
511        public Field(String json) {
512            super(json);
513        }
514
515        /**
516         * Constructs a metadate template field from a JSON object.
517         * @param jsonObject the json encoded metadate template field.
518         */
519        Field(JsonObject jsonObject) {
520            super(jsonObject);
521        }
522
523        /**
524         * Gets the data type of the field's value.
525         * @return the data type of the field's value.
526         */
527        public String getType() {
528            return this.type;
529        }
530
531        /**
532         * Sets the data type of the field's value.
533         * @param type the data type of the field's value.
534         */
535        public void setType(String type) {
536            this.type = type;
537        }
538
539        /**
540         * Gets the key of the field.
541         * @return the key of the field.
542         */
543        public String getKey() {
544            return this.key;
545        }
546
547        /**
548         * Sets the key of the field.
549         * @param key the key of the field.
550         */
551        public void setKey(String key) {
552            this.key = key;
553        }
554
555        /**
556         * Gets the display name of the field.
557         * @return the display name of the field.
558         */
559        public String getDisplayName() {
560            return this.displayName;
561        }
562
563        /**
564         * Sets the display name of the field.
565         * @param displayName the display name of the field.
566         */
567        public void setDisplayName(String displayName) {
568            this.displayName = displayName;
569        }
570
571        /**
572         * Gets is metadata template field hidden.
573         * @return is metadata template field hidden.
574         */
575        public Boolean getIsHidden() {
576            return this.isHidden;
577        }
578
579        /**
580         * Sets is metadata template field hidden.
581         * @param isHidden is metadata template field hidden?
582         */
583        public void setIsHidden(boolean isHidden) {
584            this.isHidden = isHidden;
585        }
586
587        /**
588         * Gets the description of the field.
589         * @return the description of the field.
590         */
591        public String getDescription() {
592            return this.description;
593        }
594
595        /**
596         * Sets the description of the field.
597         * @param description the description of the field.
598         */
599        public void setDescription(String description) {
600            this.description = description;
601        }
602
603        /**
604         * Gets list of possible options for enum type of the field.
605         * @return list of possible options for enum type of the field.
606         */
607        public List<String> getOptions() {
608            return this.options;
609        }
610
611        /**
612         * Sets list of possible options for enum type of the field.
613         * @param options list of possible options for enum type of the field.
614         */
615        public void setOptions(List<String> options) {
616            this.options = options;
617        }
618
619        /**
620         * {@inheritDoc}
621         */
622        @Override
623        void parseJSONMember(JsonObject.Member member) {
624            JsonValue value = member.getValue();
625            String memberName = member.getName();
626            if (memberName.equals("type")) {
627                this.type = value.asString();
628            } else if (memberName.equals("key")) {
629                this.key = value.asString();
630            } else if (memberName.equals("displayName")) {
631                this.displayName = value.asString();
632            } else if (memberName.equals("hidden")) {
633                this.isHidden = value.asBoolean();
634            } else if (memberName.equals("description")) {
635                this.description = value.asString();
636            } else if (memberName.equals("options")) {
637                this.options = new ArrayList<String>();
638                for (JsonValue key: value.asArray()) {
639                    this.options.add(key.asObject().get("key").asString());
640                }
641            }
642        }
643    }
644
645    /**
646     * Posssible operations that can be performed in a Metadata template.
647     *  <ul>
648     *      <li>Add an enum option</li>
649     *      <li>Edit an enum option</li>
650     *      <li>Remove an enum option</li>
651     *      <li>Add a field</li>
652     *      <li>Edit a field</li>
653     *      <li>Remove a field</li>
654     *      <li>Edit template</li>
655     *      <li>Reorder the enum option</li>
656     *      <li>Reorder the field list</li>
657     *  </ul>
658     */
659    public static class FieldOperation extends BoxJSONObject {
660
661        private Operation op;
662        private Field data;
663        private String fieldKey;
664        private List<String> fieldKeys;
665        private List<String> enumOptionKeys;
666        private String enumOptionKey;
667
668        /**
669         * Constructs an empty FieldOperation.
670         */
671        public FieldOperation() {
672            super();
673        }
674
675        /**
676         * Constructs a Field operation from a JSON string.
677         * @param json the json encoded metadate template field.
678         */
679        public FieldOperation(String json) {
680            super(json);
681        }
682
683        /**
684         * Constructs a Field operation from a JSON object.
685         * @param jsonObject the json encoded metadate template field.
686         */
687        FieldOperation(JsonObject jsonObject) {
688            super(jsonObject);
689        }
690
691        /**
692         * Gets the operation.
693         * @return the operation
694         */
695        public Operation getOp() {
696            return this.op;
697        }
698
699        /**
700         * Gets the data associated with the operation.
701         * @return the field object representing the data
702         */
703        public Field getData() {
704            return this.data;
705        }
706
707        /**
708         * Gets the field key.
709         * @return the field key
710         */
711        public String getFieldKey() {
712            return this.fieldKey;
713        }
714
715        /**
716         * Gets the list of field keys.
717         * @return the list of Strings
718         */
719        public List<String> getFieldKeys() {
720            return this.fieldKeys;
721        }
722
723        /**
724         * Gets the list of keys of the Enum options.
725         * @return the list of Strings
726         */
727        public List<String> getEnumOptionKeys() {
728            return this.enumOptionKeys;
729        }
730
731        /**
732         * Sets the operation.
733         * @param op the operation
734         */
735        public void setOp(Operation op) {
736            this.op = op;
737        }
738
739        /**
740         * Sets the data.
741         * @param data the Field object representing the data
742         */
743        public void setData(Field data) {
744            this.data = data;
745        }
746
747        /**
748         * Sets the field key.
749         * @param fieldKey the key of the field
750         */
751        public void setFieldKey(String fieldKey) {
752            this.fieldKey = fieldKey;
753        }
754
755        /**
756         * Sets the list of the field keys.
757         * @param fieldKeys the list of strings
758         */
759        public void setFieldKeys(List<String> fieldKeys) {
760            this.fieldKeys = fieldKeys;
761        }
762
763        /**
764         * Sets the list of the enum option keys.
765         * @param enumOptionKeys the list of Strings
766         */
767        public void setEnumOptionKeys(List<String> enumOptionKeys) {
768            this.enumOptionKeys = enumOptionKeys;
769        }
770
771        /**
772         * Gets the enum option key.
773         * @return the enum option key
774         */
775        public String getEnumOptionKey() {
776            return this.enumOptionKey;
777        }
778
779        /**
780         * Sets the enum option key.
781         * @param enumOptionKey the enum option key
782         */
783        public void setEnumOptionKey(String enumOptionKey) {
784            this.enumOptionKey = enumOptionKey;
785        }
786
787        /**
788         * {@inheritDoc}
789         */
790        @Override
791        void parseJSONMember(JsonObject.Member member) {
792            JsonValue value = member.getValue();
793            String memberName = member.getName();
794            if (memberName.equals("op")) {
795                this.op = Operation.valueOf(value.asString());
796            } else if (memberName.equals("data")) {
797                this.data = new Field(value.asObject());
798            } else if (memberName.equals("fieldKey")) {
799                this.fieldKey = value.asString();
800            } else if (memberName.equals("fieldKeys")) {
801                if (this.fieldKeys == null) {
802                    this.fieldKeys = new ArrayList<String>();
803                } else {
804                    this.fieldKeys.clear();
805                }
806
807                JsonArray array = value.asArray();
808                for (JsonValue jsonValue: array) {
809                    this.fieldKeys.add(jsonValue.asString());
810                }
811            } else if (memberName.equals("enumOptionKeys")) {
812                if (this.enumOptionKeys == null) {
813                    this.enumOptionKeys = new ArrayList<String>();
814                } else {
815                    this.enumOptionKeys.clear();
816                }
817
818                JsonArray array = value.asArray();
819                for (JsonValue jsonValue: array) {
820                    this.enumOptionKeys.add(jsonValue.asString());
821                }
822            } else if (memberName.equals("enumOptionKey")) {
823                this.enumOptionKey = value.asString();
824            }
825        }
826    }
827
828    /**
829     * Possible template operations.
830     */
831    public enum Operation {
832
833        /**
834         * Adds an enum option at the end of the enum option list for the specified field.
835         */
836        addEnumOption,
837
838        /**
839         * Edits the enum option.
840         */
841        editEnumOption,
842
843        /**
844         * Removes the specified enum option from the specified enum field.
845         */
846        removeEnumOption,
847
848        /**
849         * Adds a field at the end of the field list for the template.
850         */
851        addField,
852
853        /**
854         * Edits any number of the base properties of a field: displayName, hidden, description.
855         */
856        editField,
857
858        /**
859         * Removes the specified field from the template.
860         */
861        removeField,
862
863        /**
864         * Edits any number of the base properties of a template: displayName, hidden.
865         */
866        editTemplate,
867
868        /**
869         * Reorders the enum option list to match the requested enum option list.
870         */
871        reorderEnumOptions,
872
873        /**
874         * Reorders the field list to match the requested field list.
875         */
876        reorderFields
877    }
878}