001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.progmodel.facets.object.value;
021
022import java.text.DecimalFormat;
023import java.text.Format;
024import java.text.NumberFormat;
025import java.util.Locale;
026
027import org.apache.isis.applib.adapters.DefaultsProvider;
028import org.apache.isis.applib.adapters.EncoderDecoder;
029import org.apache.isis.applib.adapters.Parser;
030import org.apache.isis.applib.adapters.ValueSemanticsProvider;
031import org.apache.isis.applib.clock.Clock;
032import org.apache.isis.applib.profiles.Localization;
033import org.apache.isis.core.commons.authentication.AuthenticationSessionProvider;
034import org.apache.isis.core.commons.config.ConfigurationConstants;
035import org.apache.isis.core.commons.config.IsisConfiguration;
036import org.apache.isis.core.commons.exceptions.UnexpectedCallException;
037import org.apache.isis.core.commons.exceptions.UnknownTypeException;
038import org.apache.isis.core.commons.lang.LocaleUtil;
039import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
040import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
041import org.apache.isis.core.metamodel.facetapi.Facet;
042import org.apache.isis.core.metamodel.facetapi.FacetAbstract;
043import org.apache.isis.core.metamodel.facetapi.FacetHolder;
044import org.apache.isis.core.metamodel.facets.object.parseable.InvalidEntryException;
045import org.apache.isis.core.metamodel.facets.properties.defaults.PropertyDefaultFacet;
046import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector;
047import org.apache.isis.core.metamodel.spec.ObjectSpecification;
048import org.apache.isis.core.metamodel.spec.SpecificationLoader;
049
050public abstract class ValueSemanticsProviderAndFacetAbstract<T> extends FacetAbstract implements ValueSemanticsProvider<T>, EncoderDecoder<T>, Parser<T>, DefaultsProvider<T> {
051
052    private final Class<T> adaptedClass;
053    private final int typicalLength;
054    private final boolean immutable;
055    private final boolean equalByContent;
056    private final T defaultValue;
057    
058    public enum Immutability {
059        IMMUTABLE,
060        NOT_IMMUTABLE;
061
062        public static Immutability of(boolean immutable) {
063            return immutable? IMMUTABLE: NOT_IMMUTABLE;
064        }
065    }
066
067    public enum EqualByContent {
068        HONOURED,
069        NOT_HONOURED;
070
071        public static EqualByContent of(boolean equalByContent) {
072            return equalByContent? HONOURED: NOT_HONOURED;
073        }
074    }
075
076    /**
077     * Lazily looked up per {@link #getSpecification()}.
078     */
079    private ObjectSpecification specification;
080
081    private final IsisConfiguration configuration;
082    private final ValueSemanticsProviderContext context;
083
084    public ValueSemanticsProviderAndFacetAbstract(final Class<? extends Facet> adapterFacetType, final FacetHolder holder, final Class<T> adaptedClass, final int typicalLength, final Immutability immutability, final EqualByContent equalByContent, final T defaultValue, final IsisConfiguration configuration,
085            final ValueSemanticsProviderContext context) {
086        super(adapterFacetType, holder, Derivation.NOT_DERIVED);
087        this.adaptedClass = adaptedClass;
088        this.typicalLength = typicalLength;
089        this.immutable = (immutability == Immutability.IMMUTABLE);
090        this.equalByContent = (equalByContent == EqualByContent.HONOURED);
091        this.defaultValue = defaultValue;
092
093        this.configuration = configuration;
094        this.context = context;
095    }
096
097    public ObjectSpecification getSpecification() {
098        if (specification == null) {
099            specification = getSpecificationLookup().loadSpecification(getAdaptedClass());
100        }
101        return specification;
102    }
103
104    /**
105     * The underlying class that has been adapted.
106     * 
107     * <p>
108     * Used to determine whether an empty string can be parsed, (for primitive
109     * types a non-null entry is required, see {@link #mustHaveEntry()}), and
110     * potentially useful for debugging.
111     */
112    public final Class<T> getAdaptedClass() {
113        return adaptedClass;
114    }
115
116    /**
117     * We don't replace any (none no-op) facets.
118     * 
119     * <p>
120     * For example, if there is already a {@link PropertyDefaultFacet} then we
121     * shouldn't replace it.
122     */
123    @Override
124    public boolean alwaysReplace() {
125        return false;
126    }
127
128    // ///////////////////////////////////////////////////////////////////////////
129    // ValueSemanticsProvider implementation
130    // ///////////////////////////////////////////////////////////////////////////
131
132    @Override
133    public EncoderDecoder<T> getEncoderDecoder() {
134        return this;
135    }
136
137    @Override
138    public Parser<T> getParser() {
139        return this;
140    }
141
142    @Override
143    public DefaultsProvider<T> getDefaultsProvider() {
144        return this;
145    }
146
147    @Override
148    public boolean isEqualByContent() {
149        return equalByContent;
150    }
151
152    @Override
153    public boolean isImmutable() {
154        return immutable;
155    }
156
157    // ///////////////////////////////////////////////////////////////////////////
158    // Parser implementation
159    // ///////////////////////////////////////////////////////////////////////////
160
161    @Override
162    public T parseTextEntry(final Object context, final String entry, final Localization localization) {
163        if (entry == null) {
164            throw new IllegalArgumentException();
165        }
166        if (entry.trim().equals("")) {
167            if (mustHaveEntry()) {
168                throw new InvalidEntryException("An entry is required");
169            } else {
170                return null;
171            }
172        }
173        return doParse(context, entry, localization);
174    }
175
176    /**
177     * @param context
178     *            - the underlying object, or <tt>null</tt>.
179     * @param entry
180     *            - the proposed new object, as a string representation to be
181     *            parsed
182     */
183    protected T doParse(Object context, String entry) { 
184        throw new UnexpectedCallException(); 
185    } 
186
187    protected T doParse(Object context, String entry, Localization localization) { 
188        return doParse(context, entry); 
189    } 
190
191    /**
192     * Whether a non-null entry is required, used by parsing.
193     * 
194     * <p>
195     * Adapters for primitives will return <tt>true</tt>.
196     */
197    private final boolean mustHaveEntry() {
198        return adaptedClass.isPrimitive();
199    }
200
201    @Override
202    public String displayTitleOf(final Object object, final Localization localization) {
203        if (object == null) {
204            return "";
205        }
206        return titleString(object, localization);
207    }
208
209    @Override
210    public String displayTitleOf(final Object object, final String usingMask) {
211        if (object == null) {
212            return "";
213        }
214        return titleStringWithMask(object, usingMask);
215    }
216
217    /**
218     * Defaults to {@link #displayTitleOf(Object, Localization)}.
219     */
220    @Override
221    public String parseableTitleOf(final Object existing) {
222        return displayTitleOf(existing, (Localization) null);
223    }
224
225    protected String titleString(final Format formatter, final Object object) {
226        return object == null ? "" : formatter.format(object);
227    }
228
229    /**
230     * Return a string representation of aforesaid object.
231     */
232    protected abstract String titleString(Object object, Localization localization);
233
234    public abstract String titleStringWithMask(final Object value, final String usingMask);
235
236    @Override
237    public final int typicalLength() {
238        return this.typicalLength;
239    }
240
241    // ///////////////////////////////////////////////////////////////////////////
242    // DefaultsProvider implementation
243    // ///////////////////////////////////////////////////////////////////////////
244
245    @Override
246    public T getDefaultValue() {
247        return this.defaultValue;
248    }
249
250    // ///////////////////////////////////////////////////////////////////////////
251    // EncoderDecoder implementation
252    // ///////////////////////////////////////////////////////////////////////////
253
254    @Override
255    public String toEncodedString(final Object object) {
256        return doEncode(object);
257    }
258
259    @Override
260    public T fromEncodedString(final String data) {
261        return doRestore(data);
262    }
263
264    /**
265     * Hook method to perform the actual encoding.
266     */
267    protected abstract String doEncode(Object object);
268
269    /**
270     * Hook method to perform the actual restoring.
271     */
272    protected abstract T doRestore(String data);
273
274    // ///////////////////////////////////////////////////////////////////////////
275    // Helper: Locale handling
276    // ///////////////////////////////////////////////////////////////////////////
277
278    protected NumberFormat determineNumberFormat(final String suffix) {
279        final String formatRequired = getConfiguration().getString(ConfigurationConstants.ROOT + suffix);
280        if (formatRequired != null) {
281            return new DecimalFormat(formatRequired);
282        } else {
283            return NumberFormat.getNumberInstance(findLocale());
284        }
285    }
286
287    private Locale findLocale() {
288        final String localeStr = getConfiguration().getString(ConfigurationConstants.ROOT + "locale");
289
290        final Locale findLocale = LocaleUtil.findLocale(localeStr);
291        return findLocale != null ? findLocale : Locale.getDefault();
292    }
293
294    // //////////////////////////////////////////////////////////
295    // Helper: createAdapter
296    // //////////////////////////////////////////////////////////
297
298    protected ObjectAdapter createAdapter(final Class<?> type, final Object object) {
299        final ObjectSpecification specification = getSpecificationLookup().loadSpecification(type);
300        if (specification.isNotCollection()) {
301            return getAdapterManager().adapterFor(object);
302        } else {
303            throw new UnknownTypeException("not an object, is this a collection?");
304        }
305    }
306
307    // //////////////////////////////////////////////////////////
308    // Dependencies (from constructor)
309    // //////////////////////////////////////////////////////////
310
311    protected IsisConfiguration getConfiguration() {
312        return configuration;
313    }
314
315    protected ValueSemanticsProviderContext getContext() {
316        return context;
317    }
318
319    /**
320     * From {@link #getContext() context.}
321     */
322    protected AdapterManager getAdapterManager() {
323        return context.getAdapterManager();
324    }
325
326    /**
327     * From {@link #getContext() context.}
328     */
329    protected SpecificationLoader getSpecificationLookup() {
330        return context.getSpecificationLookup();
331    }
332
333    /**
334     * From {@link #getContext() context.}
335     */
336    protected ServicesInjector getDependencyInjector() {
337        return context.getDependencyInjector();
338    }
339
340    /**
341     * From {@link #getContext() context.}
342     */
343    protected AuthenticationSessionProvider getAuthenticationSessionProvider() {
344        return context.getAuthenticationSessionProvider();
345    }
346
347    // //////////////////////////////////////////////////////////
348    // Dependencies (from singleton)
349    // //////////////////////////////////////////////////////////
350
351    protected static Clock getClock() {
352        return Clock.getInstance();
353    }
354
355}