View Javadoc
1   /*
2    * MIT License
3    *
4    * Copyright (c) 2019 Technische Informationsbibliothek (TIB)
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in all
14   * copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22   * SOFTWARE.
23   *
24   * DISCLAIMER: The software has been adapted to the structure and toolset of kp-commons by Roland Lichti. The original
25   * software can be found on <https://github.com/TIBHannover/library-profile-service>.
26   */
27  package de.kaiserpfalzedv.services.dnb.marcxml;
28  
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.nio.charset.Charset;
32  import java.nio.charset.StandardCharsets;
33  import java.util.ArrayList;
34  import java.util.List;
35  import java.util.stream.Collectors;
36  
37  import org.apache.commons.io.IOUtils;
38  import org.springframework.stereotype.Service;
39  
40  import com.fasterxml.jackson.databind.ObjectMapper;
41  import com.fasterxml.jackson.dataformat.xml.XmlMapper;
42  
43  import de.kaiserpfalzedv.services.dnb.marcxml.model.DataField;
44  import de.kaiserpfalzedv.services.dnb.marcxml.model.Record;
45  import de.kaiserpfalzedv.services.dnb.marcxml.model.SearchRetrieveResponse;
46  import de.kaiserpfalzedv.services.dnb.marcxml.model.SubField;
47  import de.kaiserpfalzedv.services.dnb.model.Book;
48  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49  import jakarta.inject.Inject;
50  import jakarta.validation.constraints.NotBlank;
51  import lombok.RequiredArgsConstructor;
52  import lombok.extern.slf4j.Slf4j;
53  
54  /**
55   * <p>MarcXmlConverter -- .</p>
56   *
57   * @author Technische Informationsbibliothek (TIB) Hannover
58   * @author rlichti {@literal <rlichti@kaiserpfalz-edv.de>}
59   * @since 1.0.0  2023-01-22
60   */
61  @Service
62  @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "it*s a lombok generated constructor")
63  @RequiredArgsConstructor(onConstructor = @__(@Inject))
64  @Slf4j
65  public class MarcConverter {
66      private final XmlMapper mapper = new XmlMapper();
67      private final ObjectMapper jsonMapper;
68  
69      @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Nothing get's stored here.")
70      public List<Book> convert(final SearchRetrieveResponse response) {
71          log.info("Converting response. query='{}', count={}",
72                  response.getEchoedSearchRetrieveRequest().getQuery(), response.getNumberOfRecords());
73  
74          return response.getRecords().stream()
75                  .map(this::mapRecordToBook)
76                  .toList();
77      }
78  
79      @SuppressWarnings({ "rawtypes", "unchecked" })
80      private Book mapRecordToBook(final Record record) {
81          final Book.BookBuilder result = Book.builder();
82  
83          log.debug("Converting record. position={}, schema='{}', packing='{}', datafields={}",
84                  record.getRecordPosition(), record.getRecordSchema(), record.getRecordPacking(),
85                  record.getRecordData().getRecord().getDatafields());
86  
87          final List<DataField> data = record.getRecordData().getRecord().getDatafields();
88  
89          final List<String> isbns = new ArrayList<>();
90          final List<String> authors = new ArrayList<>();
91          for (final DataField d : data) {
92              log.trace("Working on data field. tag='{}', ind1='{}', ind2='{}' subfields={}",
93                      d.getTag(), d.getInd1(), d.getInd2(), d.getSubfield());
94  
95              switch(d.getTag()) {
96                  case "024":
97                      result.ean(this.getSubField(d, "a"));
98                      break;
99  
100                 case "020":
101                     isbns.add(this.getSubField(d, "a"));
102                     isbns.add(this.getSubField(d, "9"));
103                     break;
104 
105                 case "245":
106                     result.title(this.getSubField(d, "a"));
107                     result.subTitle(this.getSubField(d, "b"));
108                     break;
109 
110                 case "490":
111                     result.series(this.getSubField(d, "a"));
112                     break;
113 
114                 case "100":
115                 case "700":
116                     authors.add(this.getSubField(d, "a"));
117                     break;
118 
119                 case "710":
120                     result.publisher(this.getSubField(d, "a"));
121                     break;
122 
123                 case "264":
124                     result.publisher(this.getSubField(d, "b"));
125                     result.placeOfPublication(this.getSubField(d, "a"));
126                     break;
127 
128                 default:
129                     continue;
130             }
131         }
132 
133         result.authors(authors.stream().filter(f -> !f.isBlank()).collect(Collectors.toList()));
134         result.isbns(isbns.stream().filter(f -> !f.isBlank()).collect(Collectors.toList()));
135 
136         return result.build();
137     }
138 
139     private String getSubField(final DataField data, @NotBlank final String code) {
140         final List<String> result = data.getSubfield().stream()
141                 .map(f -> this.retrieveSubField(f, code))
142                 .filter(s -> !s.isBlank())
143                 .toList();
144 
145         if (! result.isEmpty()) {
146             return result.get(0);
147         }
148 
149         return "";
150     }
151 
152     private String retrieveSubField(final SubField field, final String code) {
153         if (code.equals(field.getCode())) {
154             return field.getContent();
155         }
156 
157         return "";
158     }
159 
160 
161     public List<Book> convert(final InputStream is) {
162         try {
163             return this.convert(this.mapper.readValue(is, SearchRetrieveResponse.class));
164         } catch (final IOException e) {
165             throw new LibraryLookupMarc21MappingException(e);
166         }
167     }
168 
169     public InputStream convertXml(final List<Book> books) {
170         try {
171             return IOUtils.toInputStream(this.mapper.writeValueAsString(books), Charset.defaultCharset());
172         } catch (final IOException e) {
173             throw new LibraryLookupMarc21MappingException(e);
174         }
175     }
176 
177     public InputStream convert(final List<Book> books) {
178         try {
179             return IOUtils.toInputStream(new String(this.jsonMapper.writeValueAsBytes(books), StandardCharsets.UTF_8), StandardCharsets.UTF_8);
180         } catch (final IOException e) {
181             throw new LibraryLookupMarc21MappingException(e);
182         }
183     }
184 }