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.frontend.servlets;
20  
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Comparator;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Optional;
29  import java.util.function.Function;
30  
31  import javax.xml.transform.TransformerException;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.jdom2.Element;
36  import org.mycore.common.MCRException;
37  import org.mycore.common.MCRUtils;
38  import org.mycore.common.config.MCRConfiguration2;
39  import org.mycore.common.content.MCRJDOMContent;
40  import org.mycore.datamodel.classifications2.MCRCategLinkServiceFactory;
41  import org.mycore.datamodel.classifications2.MCRCategory;
42  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
43  import org.mycore.datamodel.classifications2.MCRCategoryID;
44  import org.mycore.datamodel.classifications2.MCRLabel;
45  import org.xml.sax.SAXException;
46  
47  import jakarta.servlet.http.HttpServletRequest;
48  import jakarta.servlet.http.HttpServletResponse;
49  
50  /**
51   * This servlet provides a way to visually navigate through the tree of
52   * categories of a classification. The XML output is transformed to HTML
53   * using classificationBrowserData.xsl on the server side, then sent to
54   * the client browser, where AJAX does the rest.
55   * 
56   * @author Frank Lützenkirchen
57   */
58  public class MCRClassificationBrowser2 extends MCRServlet {
59      private static final long serialVersionUID = 1L;
60  
61      private static final Logger LOGGER = LogManager.getLogger(MCRClassificationBrowser2.class);
62  
63      protected MCRQueryAdapter getQueryAdapter(final String fieldName) {
64          MCRQueryAdapter adapter = MCRConfiguration2
65              .getOrThrow("MCR.Module-classbrowser.QueryAdapter", MCRConfiguration2::instantiateClass);
66          adapter.setFieldName(fieldName);
67          return adapter;
68      }
69  
70      protected void configureQueryAdapter(MCRQueryAdapter queryAdapter, HttpServletRequest req) {
71          queryAdapter.configure(req);
72      }
73  
74      @Override
75      public void doGetPost(MCRServletJob job) throws Exception {
76          LOGGER.info("ClassificationBrowser finished in {} ms.", MCRUtils.measure(() -> processRequest(job)).toMillis());
77      }
78  
79      private void processRequest(MCRServletJob job) throws IOException, TransformerException, SAXException {
80          HttpServletRequest req = job.getRequest();
81          Settings settings = Settings.fromRequest(req);
82  
83          LOGGER.info("ClassificationBrowser {}", settings);
84  
85          MCRCategoryID id = settings.getCategID()
86              .map(categId -> new MCRCategoryID(settings.getClassifID(), categId))
87              .orElse(MCRCategoryID.rootID(settings.getClassifID()));
88          MCRCategory category = MCRCategoryDAOFactory.getInstance().getCategory(id, 1);
89          if (category == null) {
90              job.getResponse().sendError(HttpServletResponse.SC_NOT_FOUND, "Could not find category: " + id);
91              return;
92          }
93          Element xml = getClassificationBrowserData(req, settings, category);
94          renderToHTML(job, settings, xml);
95      }
96  
97      private Element getClassificationBrowserData(HttpServletRequest req, Settings settings,
98          MCRCategory category) {
99          Element xml = new Element("classificationBrowserData");
100         xml.setAttribute("classification", settings.getClassifID());
101         xml.setAttribute("webpage", settings.getWebpage());
102         settings.getParameters().ifPresent(p -> xml.setAttribute("parameters", p));
103 
104         Optional<MCRQueryAdapter> queryAdapter = configureQueryAdapter(req, settings, xml);
105 
106         Function<MCRCategoryID, String> toIdSearchValue = settings.addClassId() ? MCRCategoryID::toString
107             : MCRCategoryID::getID;
108         List<Element> data = new ArrayList<>();
109         for (MCRCategory child : category.getChildren()) {
110             queryAdapter.ifPresent(qa -> qa.setCategory(toIdSearchValue.apply(child.getId())));
111             long numResults = queryAdapter
112                 .filter(qa -> settings.countResults())
113                 .map(MCRQueryAdapter::getResultCount)
114                 .orElse(0L);
115 
116             if ((settings.removeEmptyLeaves()) && (numResults < 1)) {
117                 continue;
118             }
119             if(settings.excludeCategories().contains(child.getId().getID())) {
120                 continue;
121             }
122 
123             Element categoryE = getCategoryElement(child, numResults, settings);
124             queryAdapter.ifPresent(qa -> categoryE.setAttribute("query", qa.getQueryAsString()));
125             data.add(categoryE);
126         }
127 
128         String objectType = queryAdapter.map(MCRQueryAdapter::getObjectType).orElse(null);
129         countLinks(settings, objectType, category, data);
130         sortCategories(settings.getSortBy(), data);
131         xml.addContent(data);
132         return xml;
133     }
134 
135     private Element getCategoryElement(MCRCategory category, long numResults,
136         Settings settings) {
137         Element categoryE = new Element("category");
138         if (settings.countResults()) {
139             categoryE.setAttribute("numResults", String.valueOf(numResults));
140         }
141 
142         categoryE.setAttribute("id", category.getId().getID());
143         categoryE.setAttribute("children", Boolean.toString(category.hasChildren()));
144 
145         if (settings.addUri() && (category.getURI() != null)) {
146             categoryE.addContent(new Element("uri").setText(category.getURI().toString()));
147         }
148 
149         addLabel(settings, category, categoryE);
150         return categoryE;
151     }
152 
153     private Optional<MCRQueryAdapter> configureQueryAdapter(HttpServletRequest req, Settings settings, Element xml) {
154         if (settings.countResults() || settings.getField().isPresent()) {
155             MCRQueryAdapter queryAdapter = getQueryAdapter(settings.getField().orElse(null));
156 
157             configureQueryAdapter(queryAdapter, req);
158             if (queryAdapter.getObjectType() != null) {
159                 xml.setAttribute("objectType", queryAdapter.getObjectType());
160             }
161             return Optional.of(queryAdapter);
162         }
163         return Optional.empty();
164     }
165 
166     /**
167      * Add label in current lang, otherwise default lang, optional with
168      * description
169      */
170     private void addLabel(Settings settings, MCRCategory child, Element category) {
171         MCRLabel label = child.getCurrentLabel()
172             .orElseThrow(() -> new MCRException("Category " + child.getId() + " has no labels."));
173 
174         category.addContent(new Element("label").setText(label.getText()));
175 
176         // if true, add description
177         if (settings.addDescription() && (label.getDescription() != null)) {
178             category.addContent(new Element("description").setText(label.getDescription()));
179         }
180     }
181 
182     /** Add link count to each category */
183     private void countLinks(Settings settings, String objectType, MCRCategory category,
184         List<Element> data) {
185         if (!settings.countLinks()) {
186             return;
187         }
188         String objType = objectType;
189         if (objType != null && objType.trim().length() == 0) {
190             objType = null;
191         }
192 
193         String classifID = category.getId().getRootID();
194         Map<MCRCategoryID, Number> count = MCRCategLinkServiceFactory.getInstance().countLinksForType(category,
195             objType, true);
196         for (Iterator<Element> it = data.iterator(); it.hasNext();) {
197             Element child = it.next();
198             MCRCategoryID childID = new MCRCategoryID(classifID, child.getAttributeValue("id"));
199             int num = (count.containsKey(childID) ? count.get(childID).intValue() : 0);
200             child.setAttribute("numLinks", String.valueOf(num));
201             if ((settings.removeEmptyLeaves()) && (num < 1)) {
202                 it.remove();
203             }
204         }
205     }
206 
207     /** Sorts by id, by label in current language, or keeps natural order */
208     private void sortCategories(String sortBy, List<Element> data) {
209         switch (sortBy) {
210         case "id":
211             data.sort(Comparator.comparing(e -> e.getAttributeValue("id")));
212             break;
213         case "label":
214             data.sort(Comparator.comparing(e -> e.getChildText("label"), String::compareToIgnoreCase));
215             break;
216         default:
217             //no sort;
218         }
219     }
220 
221     /** Sends output to client browser 
222      */
223     private void renderToHTML(MCRServletJob job, Settings settings, Element xml)
224         throws IOException, TransformerException,
225         SAXException {
226         settings.getStyle()
227             .ifPresent(style -> job.getRequest().setAttribute("XSL.Style", style)); // XSL.Style, optional
228         MCRServlet.getLayoutService().doLayout(job.getRequest(), job.getResponse(), new MCRJDOMContent(xml));
229     }
230 
231     @Override
232     protected boolean allowCrossDomainRequests() {
233         return true;
234     }
235 
236     public interface MCRQueryAdapter {
237         void setFieldName(String fieldname);
238 
239         void setRestriction(String text);
240 
241         void setCategory(String text);
242 
243         String getObjectType();
244 
245         void setObjectType(String text);
246 
247         long getResultCount();
248 
249         String getQueryAsString();
250 
251         void configure(HttpServletRequest request);
252     }
253 
254     private static class Settings {
255         private String classifID;
256 
257         private String categID;
258 
259         private boolean countResults;
260 
261         private boolean addClassId;
262 
263         private List<String> excludeCategories;
264 
265         private boolean addUri;
266 
267         private boolean removeEmptyLeaves;
268 
269         private String webpage;
270 
271         private String field;
272 
273         private String parameters;
274 
275         private boolean addDescription;
276 
277         private boolean countLinks;
278 
279         private String sortBy;
280 
281         private String style;
282 
283         static Settings fromRequest(HttpServletRequest req) {
284             Settings s = new Settings();
285             s.classifID = req.getParameter("classification");
286             s.categID = req.getParameter("category");
287             s.countResults = Boolean.parseBoolean(req.getParameter("countresults"));
288             s.addClassId = Boolean.parseBoolean(req.getParameter("addclassid"));
289             s.excludeCategories = Arrays.asList(
290                     Optional.ofNullable(req.getParameter("excludeCategories")).orElse("").split(",")
291             );
292             s.addUri = Boolean.parseBoolean(req.getParameter("adduri"));
293             s.removeEmptyLeaves = !MCRUtils.filterTrimmedNotEmpty(req.getParameter("emptyleaves"))
294                 .map(Boolean::valueOf)
295                 .orElse(true);
296             s.webpage = req.getParameter("webpage");
297             s.field = req.getParameter("field");
298             s.parameters = req.getParameter("parameters");
299             s.addDescription = Boolean.parseBoolean(req.getParameter("adddescription"));
300             s.countLinks = Boolean.parseBoolean(req.getParameter("countlinks"));
301             s.sortBy = req.getParameter("sortby");
302             s.style = req.getParameter("style");
303             return s;
304         }
305 
306         String getSortBy() {
307             return sortBy;
308         }
309 
310         Optional<String> getStyle() {
311             return MCRUtils.filterTrimmedNotEmpty(style);
312         }
313 
314         String getClassifID() {
315             return classifID;
316         }
317 
318         Optional<String> getCategID() {
319             return MCRUtils.filterTrimmedNotEmpty(categID);
320         }
321 
322         boolean countResults() {
323             return countResults;
324         }
325 
326         boolean countLinks() {
327             return countLinks;
328         }
329 
330         boolean addClassId() {
331             return addClassId;
332         }
333 
334         List<String> excludeCategories() {
335             return excludeCategories;
336         }
337 
338         boolean addUri() {
339             return addUri;
340         }
341 
342         boolean removeEmptyLeaves() {
343             return removeEmptyLeaves;
344         }
345 
346         boolean addDescription() {
347             return addDescription;
348         }
349 
350         String getWebpage() {
351             return webpage;
352         }
353 
354         Optional<String> getField() {
355             return MCRUtils.filterTrimmedNotEmpty(field);
356         }
357 
358         Optional<String> getParameters() {
359             return MCRUtils.filterTrimmedNotEmpty(parameters);
360         }
361 
362         @Override
363         public String toString() {
364             return "Settings{" +
365                 "classifID='" + classifID + '\'' +
366                 ", categID='" + categID + '\'' +
367                 ", countResults=" + countResults +
368                 ", addClassId=" + addClassId +
369                 ", excludeCategories=" + excludeCategories +
370                 ", addUri=" + addUri +
371                 ", removeEmptyLeaves=" + removeEmptyLeaves +
372                 ", webpage='" + webpage + '\'' +
373                 ", field='" + field + '\'' +
374                 ", parameters='" + parameters + '\'' +
375                 ", addDescription=" + addDescription +
376                 ", countLinks=" + countLinks +
377                 ", sortBy='" + sortBy + '\'' +
378                 ", style='" + style + '\'' +
379                 '}';
380         }
381     }
382 }