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, this.getClass().getClassLoader());
80  
81          Iterator<WriterFactory> iterator = serviceLoader.iterator();
82        
83        // use while(true) loop so that we can isolate all service loader errors from .next and .hasNext to a single service
84  
85          ArrayList<WriterFactory> factories = new ArrayList<>();
86          while (true) {
87              try {
88                  if (!iterator.hasNext())
89                      break;
90                  factories.add(iterator.next());
91              } catch(ServiceConfigurationError error) {
92                  LOG.error("Found error loading a WriterFactory", error);
93              }
94          }
95  
96          registerAll(factories.toArray(EMPTY_WRITERS));
97      }
98      
99      /**
100      * Reads the identifier specified for the given {@link WriterFactory}.
101      *
102      * @param writerClass writer class.
103      * @return identifier.
104      */
105     public static String getIdentifier(WriterFactory writerClass) {
106         return writerClass.getIdentifier();
107     }
108 
109     /**
110      * Reads the <i>MIME Type</i> specified for the given {@link WriterFactory}.
111      *
112      * @param writerClass writer class.
113      * @return MIME type.
114      */
115     public static String getMimeType(WriterFactory writerClass) {
116         if (writerClass instanceof TripleWriterFactory) {
117             return ((TripleWriterFactory)writerClass).getTripleFormat().getMimeType();
118         } else if (writerClass instanceof DecoratingWriterFactory) {
119             return null;
120         } else {
121             return reportAndGetCompatFormat(writerClass).getMimeType();
122         }
123     }
124 
125     /**
126      * @return the {@link WriterFactoryRegistry} singleton instance.
127      */
128     public static WriterFactoryRegistry getInstance() {
129         return InstanceHolder.instance;
130     }
131 
132     @SuppressWarnings("deprecation")
133     private static TripleFormat reportAndGetCompatFormat(WriterFactory f) {
134         LOG.warn("{} must implement either {} or {}.", f.getClass(), TripleWriterFactory.class, DecoratingWriterFactory.class);
135         final String mimeType = f.getMimeType();
136         RDFFormat fmt;
137         try {
138             fmt = f.getRdfFormat();
139         } catch (RuntimeException e) {
140             return TripleFormat.of(mimeType, Collections.singleton(mimeType), null,
141                     Collections.emptySet(), null, TripleFormat.NONSTANDARD);
142         }
143         if (mimeType == null || fmt.hasDefaultMIMEType(mimeType)) {
144             return TripleFormat.of(fmt);
145         }
146         //override default MIME type on mismatch
147         return TripleFormat.of(fmt.getName(), Collections.singleton(mimeType), fmt.getCharset(),
148                 fmt.getFileExtensions(), fmt.getStandardURI().stringValue(), TripleFormat.capabilities(fmt));
149     }
150 
151     private static TripleWriterFactory getCompatFactory(WriterFactory f) {
152         final TripleFormat format = reportAndGetCompatFormat(f);
153         return new TripleWriterFactory() {
154             @Override
155             public TripleFormat getTripleFormat() {
156                 return format;
157             }
158 
159             @Override
160             @SuppressWarnings("deprecation")
161             public TripleHandler getTripleWriter(OutputStream os, Settings settings) {
162                 return f.getRdfWriter(os);
163             }
164 
165             @Override
166             public Settings getSupportedSettings() {
167                 return Settings.of();
168             }
169 
170             @Override
171             public String getIdentifier() {
172                 return f.getIdentifier();
173             }
174         };
175     }
176 
177     /**
178      * Registers a new {@link WriterFactory} to the registry.
179      *
180      * @param f the writer factory to be registered.
181      * @throws IllegalArgumentException if the id or the mimetype are null
182      *                                  or empty strings or if the identifier has been already defined.
183      */
184     public void register(WriterFactory f) {
185         if (f == null)
186             throw new NullPointerException("writerClass cannot be null.");
187         registerAll(new WriterFactory[]{f});
188     }
189 
190     private void registerAll(WriterFactory[] factories) {
191         final int count = factories.length;
192         if (count == 0) {
193             return;
194         }
195         final HashMap<String, ArrayList<WriterFactory>> mimes = new HashMap<>();
196         final String[] ids = new String[count];
197 
198         for (int i = 0; i < count; i++) {
199             WriterFactory f = factories[i];
200             if (!(f instanceof BaseWriterFactory<?>)) {
201                 //backwards compatibility: view vanilla WriterFactory as TripleWriterFactory
202                 f = factories[i] = getCompatFactory(f);
203             }
204             final String id = ids[i] = f.getIdentifier();
205             if (id == null || id.trim().isEmpty()) {
206                 throw new IllegalArgumentException("Invalid identifier returned by writer " + f);
207             }
208             if (f instanceof TripleWriterFactory) {
209                 String mimeType = ((TripleWriterFactory)f).getTripleFormat().getMimeType();
210                 if (mimeType == null || mimeType.trim().isEmpty()) {
211                     throw new IllegalArgumentException("Invalid MIME type returned by writer " + f);
212                 }
213                 mimes.computeIfAbsent(mimeType, k -> new ArrayList<>()).add(f);
214             }
215         }
216 
217         final List<String> idList = Arrays.asList(ids);
218         final List<WriterFactory> factoryList = Arrays.asList(factories);
219         final Map<String, WriterFactory> idToWriter;
220         synchronized (idToWriter = this.idToWriter) {
221             for (int i = 0; i < count; i++) {
222                 String id = ids[i];
223                 if (idToWriter.putIfAbsent(id, factories[i]) != null) {
224                     idToWriter.keySet().removeAll(idList.subList(0, i));
225                     throw new IllegalArgumentException("The writer identifier is already declared: " + id);
226                 }
227             }
228         }
229         //add in bulk to reduce writes to CopyOnWriteArrayList
230         writers.addAll(factoryList);
231         identifiers.addAll(idList);
232         for (Map.Entry<String, ArrayList<WriterFactory>> entry : mimes.entrySet()) {
233             String mimeType = entry.getKey();
234             mimeTypes.add(mimeType);
235             mimeToWriter.computeIfAbsent(mimeType, k -> new CopyOnWriteArrayList<>()).addAll(entry.getValue());
236         }
237     }
238 
239     /**
240      * Verifies if a {@link WriterFactory} with given <code>id</code> identifier has been registered.
241      *
242      * @param id identifier.
243      * @return <code>true</code> if the identifier has been registered, <code>false</code> otherwise.
244      */
245     public boolean hasIdentifier(String id) {
246         synchronized (idToWriter) {
247             return idToWriter.containsKey(id);
248         }
249     }
250 
251     /**
252      * @return the list of all the specified identifiers.
253      */
254     public List<String> getIdentifiers() {
255         //no synchronized block needed for CopyOnWriteArrayList
256         return Collections.unmodifiableList(identifiers);
257     }
258 
259     /**
260      * @return the list of MIME types covered by the registered {@link WriterFactory} instances.
261      */
262     public Collection<String> getMimeTypes() {
263         //no synchronized block needed for CopyOnWriteArraySet
264         return Collections.unmodifiableCollection(mimeTypes);
265     }
266 
267     /**
268      * @return the list of all the registered {@link WriterFactory} instances.
269      */
270     public List<WriterFactory> getWriters() {
271         //no synchronized block needed for CopyOnWriteArrayList
272         return Collections.unmodifiableList(writers);
273     }
274 
275     /**
276      * Returns the {@link WriterFactory} identified by <code>id</code>.
277      *
278      * @param id the writer identifier.
279      * @return the {@link WriterFactory} matching the <code>id</code>
280      *         or <code>null</code> if not found.
281      */
282     public WriterFactory getWriterByIdentifier(String id) {
283         synchronized (idToWriter) {
284             return idToWriter.get(id);
285         }
286     }
287 
288     /**
289      * Returns all the writers matching the specified <code>mimeType</code>.
290      *
291      * @param mimeType a MIMEType.
292      * @return a list of matching writers or an empty list.
293      */
294     public Collection<WriterFactory> getWritersByMimeType(String mimeType) {
295         //no synchronized block needed for synchronized map
296         //return CopyOnWriteArrayList to avoid ConcurrentModificationExceptions on iteration
297         List<WriterFactory> list = mimeToWriter.get(mimeType);
298         return list != null ? Collections.unmodifiableList(list) : Collections.emptyList();
299     }
300 
301     /**
302      * Returns an instance of {@link FormatWriter} ready to write on the given
303      * {@link OutputStream}.
304      *
305      * @param id the identifier of the {@link FormatWriter} to instantiate.
306      * @param os the output stream.
307      * @return the not <code>null</code> {@link FormatWriter} instance.
308      * @throws NullPointerException if the <code>id</code> doesn't match any registered writer.
309      *
310      * @deprecated since 2.3. Use {@link #getWriterByIdentifier(String)}
311      * in combination with {@link TripleWriterFactory#getTripleWriter(OutputStream, Settings)} instead.
312      */
313     @Deprecated
314     public FormatWriter getWriterInstanceByIdentifier(String id, OutputStream os) {
315         return Objects.requireNonNull(getWriterByIdentifier(id),
316                 "Cannot find writer with id " + id).getRdfWriter(os);
317     }
318 
319 }