001/**
002 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
003 *   This file is part of the LDP4j Project:
004 *     http://www.ldp4j.org/
005 *
006 *   Center for Open Middleware
007 *     http://www.centeropenmiddleware.com/
008 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
009 *   Copyright (C) 2014 Center for Open Middleware.
010 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
011 *   Licensed under the Apache License, Version 2.0 (the "License");
012 *   you may not use this file except in compliance with the License.
013 *   You may obtain a copy of the License at
014 *
015 *             http://www.apache.org/licenses/LICENSE-2.0
016 *
017 *   Unless required by applicable law or agreed to in writing, software
018 *   distributed under the License is distributed on an "AS IS" BASIS,
019 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 *   See the License for the specific language governing permissions and
021 *   limitations under the License.
022 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
023 *   Artifact    : org.ldp4j.framework:ldp4j-application-api:0.2.0
024 *   Bundle      : ldp4j-application-api-0.2.0.jar
025 * #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
026 */
027package org.ldp4j.application.vocabulary;
028
029import java.lang.reflect.Array;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.Map;
033import java.util.Objects;
034import java.util.SortedMap;
035import java.util.TreeMap;
036import java.util.UUID;
037
038import static java.util.Objects.*;
039
040/**
041 * A base vocabulary implementation for creating immutable vocabularies that
042 * defines a collection of {@code ImmutableTerm}s. <br/>
043 * Purpose specific vocabularies should be implemented as follows:
044 *
045 * <pre>
046 * private static class CustomVocabulary extends
047 *    AbstractImmutableVocabulary&lt;ImmutableTerm&gt; {
048 *
049 *  private static final String NAMESPACE = &quot;http://www.example.org/vocab&quot;;
050 *  private static final String PREFERRED_PREFIX = &quot;test&quot;;
051 *
052 *  private static final String TERM_THREE = &quot;termThree&quot;;
053 *  private static final String TERM_TWO = &quot;termTwo&quot;;
054 *  private static final String TERM_ONE = &quot;termOne&quot;;
055 *
056 *  public CustomVocabulary() {
057 *    super(ImmutableTerm.class, NAMESPACE, PREFERRED_PREFIX);
058 *  }
059 *
060 *  private static final CustomVocabulary VOCABULARY;
061 *  private static final Term ONE;
062 *  private static final Term TWO;
063 *  private static final Term THREE;
064 *
065 *  static {
066 *    VOCABULARY = new CustomVocabulary();
067 *    ONE = new ImmutableTerm(VOCABULARY, TERM_ONE);
068 *    TWO = new ImmutableTerm(VOCABULARY, TERM_TWO);
069 *    THREE = new ImmutableTerm(VOCABULARY, TERM_THREE);
070 *    VOCABULARY.initialize();
071 *  }
072 * }
073 * </pre>
074 *
075 * Term related operations will fail with a {@code IllegalStateException} if
076 * they are used before the vocabulary is initialized properly.
077 *
078 * @version 1.0
079 * @since 1.0.0
080 * @author Miguel Esteban Guti&eacute;rrez
081 * @see ImmutableTerm
082 */
083public abstract class AbstractImmutableVocabulary<T extends ImmutableTerm> implements Vocabulary {
084
085  private static final long serialVersionUID = -6913490730122202939L;
086
087  private enum Status {
088    INITIALIZING("is already initialized"),
089    INITIALIZED("has not been initialized properly"),
090    ;
091    private final String explanation;
092
093    Status(String explanation) {
094      this.explanation = explanation;
095    }
096
097    String getFailure(String namespace) {
098      return "Vocabulary '"+namespace+"' "+explanation;
099    }
100
101  }
102
103  private final UUID id=UUID.randomUUID();
104
105  private final String namespace;
106  private final String prefix;
107  private final Class<T> termClass;
108
109  private final Map<String,Integer> nameOrdinal=new HashMap<String,Integer>(); // NOSONAR
110  private final SortedMap<Integer,T> terms=new TreeMap<Integer,T>(); // NOSONAR
111
112  private int ordinal=-1;
113
114  private Status status=Status.INITIALIZING;
115
116  /**
117   * Create a new instance with a term class, a namespace, and a preferred
118   * prefix.
119   *
120   * @param clazz
121   *            the type of terms held by the vocabulary.
122   * @param namespace
123   *            the namespace of the vocabulary.
124   * @param prefix
125   *            the preferred prefix of the vocabulary.
126   * @throws NullPointerException
127   *             if any of the parameters is {@code null}.
128   */
129  public AbstractImmutableVocabulary(Class<T> clazz, String namespace, String prefix) {
130    this.termClass = Objects.requireNonNull(clazz,"Term class cannot be null");
131    this.namespace = Objects.requireNonNull(namespace,"Namespace cannot be null");
132    this.prefix = Objects.requireNonNull(prefix,"Preferred prefix cannot be null");
133  }
134
135  private void checkStatus(Status status) {
136    if(!this.status.equals(status)) {
137      throw new IllegalStateException(status.getFailure(this.namespace));
138    }
139  }
140
141  private void checkInitializationPrecondition(boolean precondition, String message, Object... args) {
142    if(!precondition) {
143      String failureHeader=
144        String.format(
145          "Vocabulary '%s' (%s) initialization failure: ",
146          this.namespace,
147          getClass().getName());
148      throw new IllegalArgumentException(failureHeader.concat(String.format(message,args)));
149    }
150  }
151
152  /**
153   * Get the unique identifier of the vocabulary.
154   *
155   * @return The identifier of the vocabulary.
156   * @see java.util.UUID
157   */
158  public final UUID getId() {
159    return this.id;
160  }
161
162  /**
163   * {@inheritDoc}
164   */
165  @Override
166  public final String getNamespace() {
167    return this.namespace;
168  }
169
170  /**
171   * {@inheritDoc}
172   */
173  @Override
174  public final String getPreferredPrefix() {
175    return this.prefix;
176  }
177
178  /**
179   * {@inheritDoc}
180   */
181  @Override
182  public final T[] terms() {
183    checkStatus(Status.INITIALIZED);
184    @SuppressWarnings("unchecked")
185    T[] array=(T[])Array.newInstance(this.termClass,this.ordinal);
186    return this.terms.values().toArray(array);
187  }
188
189  /**
190   * {@inheritDoc}
191   */
192  @Override
193  public final int size() {
194    checkStatus(Status.INITIALIZED);
195    return this.ordinal+1;
196  }
197
198  /**
199   * {@inheritDoc}
200   */
201  @Override
202  public final T fromName(String name) {
203    checkStatus(Status.INITIALIZED);
204    requireNonNull(name,"Term name cannot be null");
205    Integer termOrdinal = this.nameOrdinal.get(name);
206    if(termOrdinal!=null) {
207      return this.terms.get(termOrdinal);
208    }
209    return null;
210  }
211
212  /**
213   * {@inheritDoc}
214   */
215  @Override
216  public T fromOrdinal(int ordinal) {
217    checkStatus(Status.INITIALIZED);
218    if(ordinal<0 || this.ordinal<ordinal) {
219      throw new IndexOutOfBoundsException("No term available with ordinal '"+ordinal+"'");
220    }
221    return this.terms.get(ordinal);
222  }
223
224  /**
225   * {@inheritDoc}
226   */
227  @Override
228  public final <V> T fromValue(V value) {
229    checkStatus(Status.INITIALIZED);
230    try {
231      @SuppressWarnings("unchecked")
232      TypeAdapter<T,V> adapter = (TypeAdapter<T, V>) TypeAdapter.createAdapter(this.termClass,value.getClass());
233      for(T candidate:this.terms.values()) {
234        if(value.equals(adapter.adapt(candidate))) {
235          return candidate;
236        }
237      }
238      return null;
239    } catch (CannotAdaptClassesException e) {
240      throw new UnsupportedOperationException("Class '"+this.termClass.getCanonicalName()+" cannot be transformed to '"+value.getClass().getCanonicalName()+"'",e);
241    }
242  }
243
244  /**
245   * {@inheritDoc}
246   */
247  @Override
248  public final Iterator<Term> iterator() {
249    checkStatus(Status.INITIALIZED);
250    final Iterator<? extends Term> iterator = this.terms.values().iterator();
251    return new Iterator<Term>() {
252      @Override
253      public boolean hasNext() {
254        return iterator.hasNext();
255      }
256
257      @Override
258      public Term next() {
259        return iterator.next();
260      }
261
262      @Override
263      public void remove() {
264        throw new UnsupportedOperationException("Removal not supported");
265      }
266    };
267  }
268
269  /**
270   * Allow the reservation of term names during the initialization of the
271   * vocabulary.
272   *
273   * @param name
274   *            then name of the term
275   * @return a number that represents the position in which the term was
276   *         reserved
277   * @throws IllegalArgumentException
278   *             if the specified name is not valid or it has been already
279   *             reserved.
280   * @throws IllegalStateException
281   *             if the vocabulary has been already initialized.
282   */
283  protected final int reserveTermName(String name) {
284    checkStatus(Status.INITIALIZING);
285    checkInitializationPrecondition(TermUtils.isValidTermName(name), "Object '%s' is not a valid term name",name);
286    checkInitializationPrecondition(!this.nameOrdinal.containsKey(name),"Term '%s' has been already reserved",name);
287    this.nameOrdinal.put(name, ++this.ordinal);
288    return this.ordinal;
289  }
290
291  /**
292   * Upon reservation, the method enables registering the properly built
293   * immutable term instance.
294   *
295   * @param term
296   *            the term to be registered.
297   * @throws IllegalArgumentException
298   *             if the specified term instance cannot be registered, either
299   *             because the ordinal is invalid or because another term
300   *             with the same name has already been registered.
301   * @throws IllegalStateException
302   *             if the vocabulary has been already initialized.
303   */
304  protected final <S extends ImmutableTerm> void registerTerm(S term) {
305    checkStatus(Status.INITIALIZING);
306    checkInitializationPrecondition(this.nameOrdinal.containsKey(term.name()),"Term '%s' has not been reserved",term.name());
307    checkInitializationPrecondition(term.ordinal()>=0 && term.ordinal()<=this.ordinal,"Invalid ordinal '%d' for reserved name '%s'",term.ordinal(),term.name());
308    this.terms.put(term.ordinal(),this.termClass.cast(term));
309  }
310
311  /**
312   * Complete the initialization of the vocabulary. Beyond this point the
313   * vocabulary can be put to use.
314   *
315   * @throws IllegalStateException
316   *             if the vocabulary has been already initialized or not all the
317   *             reserved names have been registered.
318   */
319  protected final void initialize() {
320    checkStatus(Status.INITIALIZING);
321    if(this.terms.size()!=this.nameOrdinal.size()) {
322      throw new IllegalStateException(
323        String.format(
324          "Vocabulary '%s' (%s) initialization failure: not all reserved names have been registered",
325          this.namespace,
326          getClass().getName()));
327    }
328    this.status=Status.INITIALIZED;
329  }
330
331}