View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.restapi.v2;
20  
21  import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_DERID;
22  import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_MCRID;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.lang.annotation.Annotation;
27  import java.nio.file.FileSystemException;
28  import java.nio.file.Files;
29  import java.util.Date;
30  import java.util.List;
31  import java.util.Optional;
32  import java.util.concurrent.TimeUnit;
33  import java.util.stream.Collectors;
34  
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  import org.jdom2.JDOMException;
38  import org.mycore.access.MCRAccessException;
39  import org.mycore.common.MCRException;
40  import org.mycore.common.config.MCRConfiguration2;
41  import org.mycore.common.content.MCRContent;
42  import org.mycore.common.content.MCRStreamContent;
43  import org.mycore.datamodel.classifications2.MCRCategoryID;
44  import org.mycore.datamodel.common.MCRXMLMetadataManager;
45  import org.mycore.datamodel.metadata.MCRDerivate;
46  import org.mycore.datamodel.metadata.MCRMetaClassification;
47  import org.mycore.datamodel.metadata.MCRMetaEnrichedLinkID;
48  import org.mycore.datamodel.metadata.MCRMetaIFS;
49  import org.mycore.datamodel.metadata.MCRMetaLangText;
50  import org.mycore.datamodel.metadata.MCRMetaLinkID;
51  import org.mycore.datamodel.metadata.MCRMetadataManager;
52  import org.mycore.datamodel.metadata.MCRObject;
53  import org.mycore.datamodel.metadata.MCRObjectID;
54  import org.mycore.datamodel.niofs.MCRPath;
55  import org.mycore.frontend.jersey.MCRCacheControl;
56  import org.mycore.restapi.annotations.MCRAccessControlExposeHeaders;
57  import org.mycore.restapi.annotations.MCRApiDraft;
58  import org.mycore.restapi.annotations.MCRParam;
59  import org.mycore.restapi.annotations.MCRParams;
60  import org.mycore.restapi.annotations.MCRRequireTransaction;
61  import org.mycore.restapi.converter.MCRContentAbstractWriter;
62  import org.mycore.restapi.converter.MCRObjectIDParamConverterProvider;
63  import org.xml.sax.SAXException;
64  
65  import com.fasterxml.jackson.annotation.JsonProperty;
66  
67  import io.swagger.v3.oas.annotations.Operation;
68  import io.swagger.v3.oas.annotations.Parameter;
69  import io.swagger.v3.oas.annotations.headers.Header;
70  import io.swagger.v3.oas.annotations.media.ArraySchema;
71  import io.swagger.v3.oas.annotations.media.Content;
72  import io.swagger.v3.oas.annotations.media.ExampleObject;
73  import io.swagger.v3.oas.annotations.media.Schema;
74  import io.swagger.v3.oas.annotations.parameters.RequestBody;
75  import io.swagger.v3.oas.annotations.responses.ApiResponse;
76  import jakarta.ws.rs.BeanParam;
77  import jakarta.ws.rs.Consumes;
78  import jakarta.ws.rs.DELETE;
79  import jakarta.ws.rs.DefaultValue;
80  import jakarta.ws.rs.FormParam;
81  import jakarta.ws.rs.GET;
82  import jakarta.ws.rs.POST;
83  import jakarta.ws.rs.PUT;
84  import jakarta.ws.rs.PATCH;
85  import jakarta.ws.rs.Path;
86  import jakarta.ws.rs.PathParam;
87  import jakarta.ws.rs.Produces;
88  import jakarta.ws.rs.core.Context;
89  import jakarta.ws.rs.core.GenericEntity;
90  import jakarta.ws.rs.core.HttpHeaders;
91  import jakarta.ws.rs.core.MediaType;
92  import jakarta.ws.rs.core.Request;
93  import jakarta.ws.rs.core.Response;
94  import jakarta.ws.rs.core.UriInfo;
95  import jakarta.xml.bind.annotation.XmlElementWrapper;
96  
97  @Path("/objects/{" + PARAM_MCRID + "}/derivates")
98  public class MCRRestDerivates {
99  
100     public static final Logger LOGGER = LogManager.getLogger();
101 
102     @Context
103     Request request;
104 
105     @Context
106     UriInfo uriInfo;
107 
108     @Parameter(example = "mir_mods_00004711")
109     @PathParam(PARAM_MCRID)
110     MCRObjectID mcrId;
111 
112     private static void validateDerivateRelation(MCRObjectID mcrId, MCRObjectID derId) {
113         MCRObjectID objectId = MCRMetadataManager.getObjectId(derId, 1, TimeUnit.DAYS);
114         if (objectId != null && !mcrId.equals(objectId)) {
115             objectId = MCRMetadataManager.getObjectId(derId, 0, TimeUnit.SECONDS);
116         }
117         if (mcrId.equals(objectId)) {
118             return;
119         }
120         if (objectId == null) {
121             throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
122                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NOT_FOUND)
123                 .withMessage("MCRDerivate " + derId + " not found")
124                 .toException();
125         }
126         throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
127             .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NOT_FOUND_IN_OBJECT)
128             .withMessage("MCRDerivate " + derId + " not found in object " + mcrId + ".")
129             .toException();
130     }
131 
132     @GET
133     @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
134     @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
135         sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
136     @Operation(
137         summary = "Lists all derivates in the given object",
138         responses = {
139             @ApiResponse(
140                 content = @Content(array = @ArraySchema(schema = @Schema(implementation = MCRMetaLinkID.class)))),
141             @ApiResponse(responseCode = "" + MCRObjectIDParamConverterProvider.CODE_INVALID,
142                 description = MCRObjectIDParamConverterProvider.MSG_INVALID),
143 
144         },
145         tags = MCRRestUtils.TAG_MYCORE_DERIVATE)
146     @XmlElementWrapper(name = "derobjects")
147     public Response listDerivates()
148         throws IOException {
149         long modified = MCRXMLMetadataManager.instance().getLastModified(mcrId);
150         if (modified < 0) {
151             throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
152                 .withErrorCode(MCRErrorCodeConstants.MCROBJECT_NOT_FOUND)
153                 .withMessage("MCRObject " + mcrId + " not found")
154                 .toException();
155         }
156         Date lastModified = new Date(modified);
157         Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
158         if (cachedResponse.isPresent()) {
159             return cachedResponse.get();
160         }
161         MCRObject obj = MCRMetadataManager.retrieveMCRObject(mcrId);
162         List<MCRMetaEnrichedLinkID> derivates = obj.getStructure().getDerivates();
163         return Response.ok()
164             .entity(new GenericEntity<List<MCRMetaEnrichedLinkID>>(derivates) {
165             })
166             .lastModified(lastModified)
167             .build();
168     }
169 
170     @GET
171     @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
172     @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
173         sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
174     @Operation(
175         summary = "Returns given derivate in the given object",
176         tags = MCRRestUtils.TAG_MYCORE_DERIVATE)
177     @Path("/{" + PARAM_DERID + "}")
178     public Response getDerivate(@Parameter(example = "mir_derivate_00004711") @PathParam(PARAM_DERID) MCRObjectID derid)
179         throws IOException {
180         validateDerivateRelation(mcrId, derid);
181         long modified = MCRXMLMetadataManager.instance().getLastModified(derid);
182         Date lastModified = new Date(modified);
183         Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lastModified);
184         if (cachedResponse.isPresent()) {
185             return cachedResponse.get();
186         }
187         MCRContent mcrContent = MCRXMLMetadataManager.instance().retrieveContent(derid);
188         return Response.ok()
189             .entity(mcrContent,
190                 new Annotation[] { MCRParams.Factory
191                     .get(MCRParam.Factory.get(MCRContentAbstractWriter.PARAM_OBJECTTYPE, derid.getTypeId())) })
192             .lastModified(lastModified)
193             .build();
194     }
195 
196     @PUT
197     @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8" })
198     @Operation(summary = "Creates or updates MCRDerivate with the body of this request",
199         tags = MCRRestUtils.TAG_MYCORE_DERIVATE,
200         responses = {
201             @ApiResponse(responseCode = "400",
202                 content = { @Content(mediaType = MediaType.TEXT_PLAIN) },
203                 description = "'Invalid body content' or 'MCRObjectID mismatch'"),
204             @ApiResponse(responseCode = "201", description = "MCRDerivate successfully created"),
205             @ApiResponse(responseCode = "204", description = "MCRDerivate successfully updated"),
206         })
207     @MCRRequireTransaction
208     @Path("/{" + PARAM_DERID + "}")
209     public Response updateDerivate(
210         @Parameter(example = "mir_derivate_00004711") @PathParam(PARAM_DERID) MCRObjectID derid,
211         @Parameter(required = true,
212             description = "MCRObject XML",
213             examples = @ExampleObject("<mycoreobject ID=\"{mcrid}\" ..>\n...\n</mycorobject>")) InputStream xmlSource)
214         throws IOException {
215         //check preconditions
216         try {
217             long lastModified = MCRXMLMetadataManager.instance().getLastModified(derid);
218             if (lastModified >= 0) {
219                 Date lmDate = new Date(lastModified);
220                 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request, lmDate);
221                 if (cachedResponse.isPresent()) {
222                     return cachedResponse.get();
223                 }
224             }
225         } catch (Exception e) {
226             //ignore errors as PUT is idempotent
227         }
228         boolean create = true;
229         if (MCRMetadataManager.exists(derid)) {
230             validateDerivateRelation(mcrId, derid);
231             create = false;
232         }
233         MCRStreamContent inputContent = new MCRStreamContent(xmlSource, null, MCRDerivate.ROOT_NAME);
234         MCRDerivate derivate;
235         try {
236             derivate = new MCRDerivate(inputContent.asXML());
237             derivate.validate();
238         } catch (JDOMException | SAXException | MCRException e) {
239             throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
240                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_INVALID)
241                 .withMessage("MCRDerivate " + derid + " is not valid")
242                 .withDetail(e.getMessage())
243                 .withCause(e)
244                 .toException();
245         }
246         if (!derid.equals(derivate.getId())) {
247             throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
248                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_ID_MISMATCH)
249                 .withMessage("MCRDerivate " + derid + " cannot be overwritten by " + derivate.getId() + ".")
250                 .toException();
251         }
252         try {
253             if (create) {
254                 MCRMetadataManager.create(derivate);
255                 MCRPath rootDir = MCRPath.getPath(derid.toString(), "/");
256                 if (Files.notExists(rootDir)) {
257                     rootDir.getFileSystem().createRoot(derid.toString());
258                 }
259                 return Response.status(Response.Status.CREATED).build();
260             } else {
261                 MCRMetadataManager.update(derivate);
262                 return Response.status(Response.Status.NO_CONTENT).build();
263             }
264         } catch (MCRAccessException e) {
265             throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
266                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NO_PERMISSION)
267                 .withMessage("You may not modify or create MCRDerivate " + derid + ".")
268                 .withDetail(e.getMessage())
269                 .withCause(e)
270                 .toException();
271         }
272     }
273 
274     @DELETE
275     @Operation(summary = "Deletes MCRDerivate {" + PARAM_DERID + "}",
276         tags = MCRRestUtils.TAG_MYCORE_DERIVATE,
277         responses = {
278             @ApiResponse(responseCode = "204", description = "MCRDerivate successfully deleted"),
279         })
280     @MCRRequireTransaction
281     @Path("/{" + PARAM_DERID + "}")
282     public Response deleteDerivate(
283         @Parameter(example = "mir_derivate_00004711") @PathParam(PARAM_DERID) MCRObjectID derid) {
284         if (!MCRMetadataManager.exists(derid)) {
285             throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
286                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NOT_FOUND)
287                 .withMessage("MCRDerivate " + derid + " not found")
288                 .toException();
289         }
290         try {
291             MCRMetadataManager.deleteMCRDerivate(derid);
292             return Response.noContent().build();
293         } catch (MCRAccessException e) {
294             throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
295                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NO_PERMISSION)
296                 .withMessage("You may not delete MCRDerivate " + derid + ".")
297                 .withDetail(e.getMessage())
298                 .withCause(e)
299                 .toException();
300         }
301     }
302 
303     @POST
304     @Operation(
305         summary = "Adds a new derivate (with defaults for 'display-enabled', 'main-doc', 'label') in the given object",
306         responses = @ApiResponse(responseCode = "201",
307             headers = @Header(name = HttpHeaders.LOCATION, description = "URL of the new derivate")),
308         tags = MCRRestUtils.TAG_MYCORE_DERIVATE)
309     @MCRRequireTransaction
310     @MCRAccessControlExposeHeaders(HttpHeaders.LOCATION)
311     public Response createDefaultDerivate() {
312         return doCreateDerivate(new DerivateMetadata());
313     }
314 
315     @POST
316     @Operation(
317         summary = "Adds a new derivate in the given object",
318         responses = @ApiResponse(responseCode = "201",
319             headers = @Header(name = HttpHeaders.LOCATION, description = "URL of the new derivate")),
320         tags = MCRRestUtils.TAG_MYCORE_DERIVATE)
321     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
322     @RequestBody(required = true,
323         content = @Content(mediaType = MediaType.APPLICATION_FORM_URLENCODED,
324             schema = @Schema(implementation = DerivateMetadata.class)))
325     @MCRRequireTransaction
326     @MCRAccessControlExposeHeaders(HttpHeaders.LOCATION)
327     public Response createDerivate(@BeanParam DerivateMetadata der) {
328         return doCreateDerivate(der);
329     }
330 
331     private Response doCreateDerivate(@BeanParam DerivateMetadata der) {
332         LOGGER.debug(der);
333         String projectID = mcrId.getProjectId();
334         MCRObjectID derId = MCRObjectID.getNextFreeId(projectID + "_derivate");
335         MCRDerivate derivate = new MCRDerivate();
336         derivate.setId(derId);
337 
338         derivate.setOrder(der.getOrder());
339 
340         derivate.getDerivate().getClassifications()
341             .addAll(der.getClassifications().stream()
342                 .map(categId -> new MCRMetaClassification("classification", 0, null, categId))
343                 .collect(Collectors.toList()));
344 
345         derivate.getDerivate().getTitles()
346             .addAll(der.getTitles().stream()
347                 .map(DerivateTitle::toMetaLangText)
348                 .collect(Collectors.toList()));
349 
350         String schema = MCRConfiguration2.getString("MCR.Metadata.Config.derivate")
351             .orElse("datamodel-derivate.xml")
352             .replaceAll(".xml", ".xsd");
353         derivate.setSchema(schema);
354 
355         MCRMetaLinkID linkId = new MCRMetaLinkID();
356         linkId.setSubTag("linkmeta");
357         linkId.setReference(mcrId, null, null);
358         derivate.getDerivate().setLinkMeta(linkId);
359 
360         MCRMetaIFS ifs = new MCRMetaIFS();
361         ifs.setSubTag("internal");
362         ifs.setSourcePath(null);
363         ifs.setMainDoc(der.getMainDoc());
364         derivate.getDerivate().setInternals(ifs);
365 
366         LOGGER.debug("Creating new derivate with ID {}", derId);
367         try {
368             MCRMetadataManager.create(derivate);
369         } catch (MCRAccessException e) {
370             throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
371                 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NO_PERMISSION)
372                 .withMessage("You may not create MCRDerivate " + derId + ".")
373                 .withDetail(e.getMessage())
374                 .withCause(e)
375                 .toException();
376         }
377         MCRPath rootDir = MCRPath.getPath(derId.toString(), "/");
378         if (Files.notExists(rootDir)) {
379             try {
380                 rootDir.getFileSystem().createRoot(derId.toString());
381             } catch (FileSystemException e) {
382                 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
383                     .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_CREATE_DIRECTORY)
384                     .withMessage("Could not create root directory for MCRDerivate " + derId + ".")
385                     .withDetail(e.getMessage())
386                     .withCause(e)
387                     .toException();
388             }
389         }
390         return Response.created(uriInfo.getAbsolutePathBuilder().path(derId.toString()).build()).build();
391     }
392 
393     @PATCH
394     @Operation(
395         summary = "Updates the metadata (or partial metadata) of the given derivate",
396         responses = @ApiResponse(responseCode = "204"),
397         tags = MCRRestUtils.TAG_MYCORE_DERIVATE)
398     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
399     @RequestBody(required = true,
400         content = @Content(mediaType = MediaType.APPLICATION_FORM_URLENCODED,
401             schema = @Schema(implementation = DerivateMetadata.class)))
402     @MCRRequireTransaction
403     @MCRAccessControlExposeHeaders(HttpHeaders.LOCATION)
404     @Path("/{" + PARAM_DERID + "}")
405     @MCRApiDraft("MCRPatchDerivate")
406     public Response patchDerivate(@BeanParam DerivateMetadata der,
407         @Parameter(example = "mir_derivate_00004711") @PathParam(PARAM_DERID) MCRObjectID derid) {
408 
409         LOGGER.debug(der);
410         MCRDerivate derivate = MCRMetadataManager.retrieveMCRDerivate(derid);
411         boolean modified = false;
412 
413         if (der.getOrder() != -1
414             && derivate.getOrder() != der.getOrder()) {
415             modified = true;
416             derivate.setOrder(der.getOrder());
417         }
418 
419         if (der.getMainDoc() != null
420             && !der.getMainDoc().equals(derivate.getDerivate().getInternals().getMainDoc())) {
421             modified = true;
422             derivate.getDerivate().getInternals().setMainDoc(der.getMainDoc());
423         }
424 
425         List<MCRCategoryID> oldClassifications = derivate.getDerivate().getClassifications().stream()
426             .map(x -> MCRCategoryID.fromString(x.getClassId() + ":" + x.getCategId()))
427             .collect(Collectors.toList());
428         if (!der.getClassifications().isEmpty()
429             && (oldClassifications.size() != der.getClassifications().size()
430                 || !oldClassifications.containsAll(der.getClassifications()))) {
431             modified = true;
432             derivate.getDerivate().getClassifications().clear();
433             derivate.getDerivate().getClassifications()
434                 .addAll(der.getClassifications().stream()
435                     .map(categId -> new MCRMetaClassification("classification", 0, null, categId))
436                     .collect(Collectors.toList()));
437         }
438 
439         List<MCRMetaLangText> newTitles = der.getTitles().stream()
440             .map(DerivateTitle::toMetaLangText)
441             .collect(Collectors.toList());
442         if (!newTitles.isEmpty()
443             && (derivate.getDerivate().getTitleSize() != newTitles.size()
444                 || !derivate.getDerivate().getTitles().containsAll(newTitles))) {
445             modified = true;
446             derivate.getDerivate().getTitles().clear();
447             derivate.getDerivate().getTitles().addAll(newTitles);
448         }
449 
450         if (modified) {
451             try {
452                 MCRMetadataManager.update(derivate);
453             } catch (MCRAccessException e) {
454                 throw MCRErrorResponse.fromStatus(Response.Status.FORBIDDEN.getStatusCode())
455                     .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NO_PERMISSION)
456                     .withMessage("You may not update MCRDerivate " + derivate.getId() + ".")
457                     .withDetail(e.getMessage())
458                     .withCause(e)
459                     .toException();
460             }
461         }
462         return Response.noContent().build();
463     }
464 
465     @PUT
466     @Path("/{" + PARAM_DERID + "}/try")
467     @Operation(summary = "pre-flight target to test write operation on {" + PARAM_DERID + "}",
468         tags = MCRRestUtils.TAG_MYCORE_DERIVATE,
469         responses = {
470             @ApiResponse(responseCode = "202", description = "You have write permission"),
471             @ApiResponse(responseCode = "401",
472                 description = "You do not have write permission and need to authenticate first"),
473             @ApiResponse(responseCode = "403", description = "You do not have write permission"),
474         })
475     public Response testUpdateDerivate(@PathParam(PARAM_DERID) MCRObjectID id)
476         throws IOException {
477         return Response.status(Response.Status.ACCEPTED).build();
478     }
479 
480     @DELETE
481     @Path("/{" + PARAM_DERID + "}/try")
482     @Operation(summary = "pre-flight target to test delete operation on {" + PARAM_DERID + "}",
483         tags = MCRRestUtils.TAG_MYCORE_DERIVATE,
484         responses = {
485             @ApiResponse(responseCode = "202", description = "You have delete permission"),
486             @ApiResponse(responseCode = "401",
487                 description = "You do not have delete permission and need to authenticate first"),
488             @ApiResponse(responseCode = "403", description = "You do not have delete permission"),
489         })
490     public Response testDeleteDerivate(@PathParam(PARAM_DERID) MCRObjectID id)
491         throws IOException {
492         return Response.status(Response.Status.ACCEPTED).build();
493     }
494 
495     public static class DerivateTitle {
496         private String lang;
497 
498         private String text;
499 
500         //Jersey can use this method without further configuration
501         public static DerivateTitle fromString(String value) {
502             final DerivateTitle derivateTitle = new DerivateTitle();
503             if (value.length() >= 4 && value.charAt(0) == '(') {
504                 int pos = value.indexOf(')');
505                 if (pos > 1) {
506                     derivateTitle.setLang(value.substring(1, pos));
507                     derivateTitle.setText(value.substring(pos + 1));
508                     return derivateTitle;
509                 }
510             }
511             derivateTitle.setText(value);
512             return derivateTitle;
513         }
514 
515         public String getLang() {
516             return lang;
517         }
518 
519         public void setLang(String lang) {
520             this.lang = lang;
521         }
522 
523         public String getText() {
524             return text;
525         }
526 
527         public void setText(String text) {
528             this.text = text;
529         }
530 
531         public MCRMetaLangText toMetaLangText() {
532             return new MCRMetaLangText("title", getLang(), null, 0, null, getText());
533         }
534     }
535 
536     public static class DerivateMetadata {
537         private String mainDoc;
538 
539         private int order = 1;
540 
541         private List<MCRCategoryID> classifications = List.of();
542 
543         private List<DerivateTitle> titles = List.of();
544 
545         String getMainDoc() {
546             return mainDoc;
547         }
548 
549         @FormParam("maindoc")
550         @JsonProperty("maindoc")
551         public void setMainDoc(String mainDoc) {
552             this.mainDoc = mainDoc;
553         }
554 
555         public int getOrder() {
556             return order;
557         }
558 
559         @JsonProperty
560         @FormParam("order")
561         @DefaultValue("1")
562         public void setOrder(int order) {
563             this.order = order;
564         }
565 
566         public List<MCRCategoryID> getClassifications() {
567             return classifications;
568         }
569 
570         @JsonProperty
571         @FormParam("classification")
572         public void setClassifications(List<MCRCategoryID> classifications) {
573             this.classifications = classifications;
574         }
575 
576         public List<DerivateTitle> getTitles() {
577             return titles;
578         }
579 
580         @FormParam("title")
581         public void setTitles(List<DerivateTitle> titles) {
582             this.titles = titles;
583         }
584 
585     }
586 }