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 }