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