View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *  http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.any23.writer;
19  
20  import java.io.OutputStream;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.ServiceConfigurationError;
31  import java.util.ServiceLoader;
32  import java.util.concurrent.CopyOnWriteArrayList;
33  import java.util.concurrent.CopyOnWriteArraySet;
34  
35  import org.apache.any23.configuration.Settings;
36  import org.eclipse.rdf4j.rio.RDFFormat;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * Registry class for {@link WriterFactory}s.
42   *
43   * @author Michele Mostarda (mostarda@fbk.eu)
44   * @author Hans Brende (hansbrende@apache.org)
45   */
46  public class WriterFactoryRegistry {
47  
48      private static final Logger LOG = LoggerFactory.getLogger(WriterFactoryRegistry.class);
49  
50      /**
51       * Singleton instance.
52       */
53      private static class InstanceHolder {
54          private static final WriterFactoryRegistry.html#WriterFactoryRegistry">WriterFactoryRegistry instance = new WriterFactoryRegistry();
55      }
56  
57      private static final WriterFactoryriterFactory">WriterFactory[] EMPTY_WRITERS = new WriterFactory[0];
58  
59      /**
60       * List of registered writers.
61       */
62      private final List<WriterFactory> writers = new CopyOnWriteArrayList<>();
63  
64      /**
65       * MIME Type to {@link WriterFactory} class.
66       */
67      private final Map<String, List<WriterFactory>> mimeToWriter = Collections.synchronizedMap(new HashMap<>());
68  
69      /**
70       * Identifier to {@link WriterFactory} class.
71       */
72      private final Map<String, WriterFactory> idToWriter = new HashMap<>();
73  
74      private final List<String> identifiers = new CopyOnWriteArrayList<>();
75  
76      private final Collection<String> mimeTypes = new CopyOnWriteArraySet<>();
77  
78      public WriterFactoryRegistry() {
79          ServiceLoader<WriterFactory> serviceLoader = java.util.ServiceLoader.load(WriterFactory.class,
80                  this.getClass().getClassLoader());
81  
82          Iterator<WriterFactory> iterator = serviceLoader.iterator();
83  
84          // use while(true) loop so that we can isolate all service loader errors from .next and .hasNext to a single
85          // service
86  
87          ArrayList<WriterFactory> factories = new ArrayList<>();
88          while (true) {
89              try {
90                  if (!iterator.hasNext())
91                      break;
92                  factories.add(iterator.next());
93              } catch (ServiceConfigurationError error) {
94                  LOG.error("Found error loading a WriterFactory", error);
95              }
96          }
97  
98          registerAll(factories.toArray(EMPTY_WRITERS));
99      }
100 
101     /**
102      * Reads the identifier specified for the given {@link WriterFactory}.
103      *
104      * @param writerClass
105      *            writer class.
106      * 
107      * @return identifier.
108      */
109     public static String getIdentifier(WriterFactory writerClass) {
110         return writerClass.getIdentifier();
111     }
112 
113     /**
114      * Reads the <i>MIME Type</i> specified for the given {@link WriterFactory}.
115      *
116      * @param writerClass
117      *            writer class.
118      * 
119      * @return MIME type.
120      */
121     public static String getMimeType(WriterFactory writerClass) {
122         if (writerClass instanceof TripleWriterFactory) {
123             return ((TripleWriterFactory) writerClass).getTripleFormat().getMimeType();
124         } else if (writerClass instanceof DecoratingWriterFactory) {
125             return null;
126         } else {
127             return reportAndGetCompatFormat(writerClass).getMimeType();
128         }
129     }
130 
131     /**
132      * @return the {@link WriterFactoryRegistry} singleton instance.
133      */
134     public static WriterFactoryRegistry getInstance() {
135         return InstanceHolder.instance;
136     }
137 
138     @SuppressWarnings("deprecation")
139     private static TripleFormat reportAndGetCompatFormat(WriterFactory f) {
140         LOG.warn("{} must implement either {} or {}.", f.getClass(), TripleWriterFactory.class,
141                 DecoratingWriterFactory.class);
142         final String mimeType = f.getMimeType();
143         RDFFormat fmt;
144         try {
145             fmt = f.getRdfFormat();
146         } catch (RuntimeException e) {
147             return TripleFormat.of(mimeType, Collections.singleton(mimeType), null, Collections.emptySet(), null,
148                     TripleFormat.NONSTANDARD);
149         }
150         if (mimeType == null || fmt.hasDefaultMIMEType(mimeType)) {
151             return TripleFormat.of(fmt);
152         }
153         // override default MIME type on mismatch
154         return TripleFormat.of(fmt.getName(), Collections.singleton(mimeType), fmt.getCharset(),
155                 fmt.getFileExtensions(), fmt.getStandardURI().stringValue(), TripleFormat.capabilities(fmt));
156     }
157 
158     private static TripleWriterFactory getCompatFactory(WriterFactory f) {
159         final TripleFormat format = reportAndGetCompatFormat(f);
160         return new TripleWriterFactory() {
161             @Override
162             public TripleFormat getTripleFormat() {
163                 return format;
164             }
165 
166             @Override
167             @SuppressWarnings("deprecation")
168             public TripleHandler getTripleWriter(OutputStream os, Settings settings) {
169                 return f.getRdfWriter(os);
170             }
171 
172             @Override
173             public Settings getSupportedSettings() {
174                 return Settings.of();
175             }
176 
177             @Override
178             public String getIdentifier() {
179                 return f.getIdentifier();
180             }
181         };
182     }
183 
184     /**
185      * Registers a new {@link WriterFactory} to the registry.
186      *
187      * @param f
188      *            the writer factory to be registered.
189      * 
190      * @throws IllegalArgumentException
191      *             if the id or the mimetype are null or empty strings or if the identifier has been already defined.
192      */
193     public void register(WriterFactory f) {
194         if (f == null)
195             throw new NullPointerException("writerClass cannot be null.");
196         registerAll(new WriterFactory[] { f });
197     }
198 
199     private void registerAll(WriterFactory[] factories) {
200         final int count = factories.length;
201         if (count == 0) {
202             return;
203         }
204         final HashMap<String, ArrayList<WriterFactory>> mimes = new HashMap<>();
205         final String[] ids = new String[count];
206 
207         for (int i = 0; i < count; i++) {
208             WriterFactory f = factories[i];
209             if (!(f instanceof BaseWriterFactory<?>)) {
210                 // backwards compatibility: view vanilla WriterFactory as TripleWriterFactory
211                 f = factories[i] = getCompatFactory(f);
212             }
213             final String id = ids[i] = f.getIdentifier();
214             if (id == null || id.trim().isEmpty()) {
215                 throw new IllegalArgumentException("Invalid identifier returned by writer " + f);
216             }
217             if (f instanceof TripleWriterFactory) {
218                 String mimeType = ((TripleWriterFactory) f).getTripleFormat().getMimeType();
219                 if (mimeType == null || mimeType.trim().isEmpty()) {
220                     throw new IllegalArgumentException("Invalid MIME type returned by writer " + f);
221                 }
222                 mimes.computeIfAbsent(mimeType, k -> new ArrayList<>()).add(f);
223             }
224         }
225 
226         final List<String> idList = Arrays.asList(ids);
227         final List<WriterFactory> factoryList = Arrays.asList(factories);
228         final Map<String, WriterFactory> idToWriter;
229         synchronized (idToWriter = this.idToWriter) {
230             for (int i = 0; i < count; i++) {
231                 String id = ids[i];
232                 if (idToWriter.putIfAbsent(id, factories[i]) != null) {
233                     idToWriter.keySet().removeAll(idList.subList(0, i));
234                     throw new IllegalArgumentException("The writer identifier is already declared: " + id);
235                 }
236             }
237         }
238         // add in bulk to reduce writes to CopyOnWriteArrayList
239         writers.addAll(factoryList);
240         identifiers.addAll(idList);
241         for (Map.Entry<String, ArrayList<WriterFactory>> entry : mimes.entrySet()) {
242             String mimeType = entry.getKey();
243             mimeTypes.add(mimeType);
244             mimeToWriter.computeIfAbsent(mimeType, k -> new CopyOnWriteArrayList<>()).addAll(entry.getValue());
245         }
246     }
247 
248     /**
249      * Verifies if a {@link WriterFactory} with given <code>id</code> identifier has been registered.
250      *
251      * @param id
252      *            identifier.
253      * 
254      * @return <code>true</code> if the identifier has been registered, <code>false</code> otherwise.
255      */
256     public boolean hasIdentifier(String id) {
257         synchronized (idToWriter) {
258             return idToWriter.containsKey(id);
259         }
260     }
261 
262     /**
263      * @return the list of all the specified identifiers.
264      */
265     public List<String> getIdentifiers() {
266         // no synchronized block needed for CopyOnWriteArrayList
267         return Collections.unmodifiableList(identifiers);
268     }
269 
270     /**
271      * @return the list of MIME types covered by the registered {@link WriterFactory} instances.
272      */
273     public Collection<String> getMimeTypes() {
274         // no synchronized block needed for CopyOnWriteArraySet
275         return Collections.unmodifiableCollection(mimeTypes);
276     }
277 
278     /**
279      * @return the list of all the registered {@link WriterFactory} instances.
280      */
281     public List<WriterFactory> getWriters() {
282         // no synchronized block needed for CopyOnWriteArrayList
283         return Collections.unmodifiableList(writers);
284     }
285 
286     /**
287      * Returns the {@link WriterFactory} identified by <code>id</code>.
288      *
289      * @param id
290      *            the writer identifier.
291      * 
292      * @return the {@link WriterFactory} matching the <code>id</code> or <code>null</code> if not found.
293      */
294     public WriterFactory getWriterByIdentifier(String id) {
295         synchronized (idToWriter) {
296             return idToWriter.get(id);
297         }
298     }
299 
300     /**
301      * Returns all the writers matching the specified <code>mimeType</code>.
302      *
303      * @param mimeType
304      *            a MIMEType.
305      * 
306      * @return a list of matching writers or an empty list.
307      */
308     public Collection<WriterFactory> getWritersByMimeType(String mimeType) {
309         // no synchronized block needed for synchronized map
310         // return CopyOnWriteArrayList to avoid ConcurrentModificationExceptions on iteration
311         List<WriterFactory> list = mimeToWriter.get(mimeType);
312         return list != null ? Collections.unmodifiableList(list) : Collections.emptyList();
313     }
314 
315     /**
316      * Returns an instance of {@link FormatWriter} ready to write on the given {@link OutputStream}.
317      *
318      * @param id
319      *            the identifier of the {@link FormatWriter} to instantiate.
320      * @param os
321      *            the output stream.
322      * 
323      * @return the not <code>null</code> {@link FormatWriter} instance.
324      * 
325      * @throws NullPointerException
326      *             if the <code>id</code> doesn't match any registered writer.
327      *
328      * @deprecated since 2.3. Use {@link #getWriterByIdentifier(String)} in combination with
329      *             {@link TripleWriterFactory#getTripleWriter(OutputStream, Settings)} instead.
330      */
331     @Deprecated
332     public FormatWriter getWriterInstanceByIdentifier(String id, OutputStream os) {
333         return Objects.requireNonNull(getWriterByIdentifier(id), "Cannot find writer with id " + id).getRdfWriter(os);
334     }
335 
336 }