001/*
002 * CREDIT SUISSE IS WILLING TO LICENSE THIS SPECIFICATION TO YOU ONLY UPON THE CONDITION THAT YOU
003 * ACCEPT ALL OF THE TERMS CONTAINED IN THIS AGREEMENT. PLEASE READ THE TERMS AND CONDITIONS OF THIS
004 * AGREEMENT CAREFULLY. BY DOWNLOADING THIS SPECIFICATION, YOU ACCEPT THE TERMS AND CONDITIONS OF
005 * THE AGREEMENT. IF YOU ARE NOT WILLING TO BE BOUND BY IT, SELECT THE "DECLINE" BUTTON AT THE
006 * BOTTOM OF THIS PAGE. Specification: JSR-354 Money and Currency API ("Specification") Copyright
007 * (c) 2012-2013, Credit Suisse All rights reserved.
008 */
009package org.javamoney.moneta.internal;
010
011import java.util.ArrayList;
012import java.util.Collections;
013import java.util.Comparator;
014import java.util.List;
015import java.util.Map;
016import java.util.Set;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.logging.Level;
019import java.util.logging.Logger;
020
021import javax.money.MonetaryAmount;
022import javax.money.MonetaryAmountFactory;
023import javax.money.MonetaryContext;
024import javax.money.MonetaryContext.AmountFlavor;
025import javax.money.MonetaryException;
026import javax.money.spi.Bootstrap;
027import javax.money.spi.MonetaryAmountFactoryProviderSpi;
028import javax.money.spi.MonetaryAmountFactoryProviderSpi.QueryInclusionPolicy;
029import javax.money.spi.MonetaryAmountsSpi;
030
031import org.javamoney.moneta.ServicePriority;
032
033/**
034 * Default implementation ot {@link javax.money.spi.MonetaryAmountsSpi} loading the SPIs on startup initially once, using the
035 * JSR's {@link javax.money.spi.Bootstrap} mechanism.
036 */
037public class DefaultMonetaryAmountsSpi implements MonetaryAmountsSpi {
038
039        private Map<Class<? extends MonetaryAmount>, MonetaryAmountFactoryProviderSpi<?>> factories = new ConcurrentHashMap<>();
040
041        private Class<? extends MonetaryAmount> configuredDefaultAmountType = loadDefaultAmountType();
042
043        private static final Comparator<MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount>> CONTEXT_COMPARATOR = new Comparator<MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount>>() {
044
045                @Override
046                public int compare(
047                                MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f1,
048                                MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f2) {
049                        int compare = 0;
050                        MonetaryContext c1 = f1.getMaximalMonetaryContext();
051                        MonetaryContext c2 = f2.getMaximalMonetaryContext();
052                        if (c1.getAmountFlavor() == AmountFlavor.PRECISION
053                                        && c2.getAmountFlavor() != AmountFlavor.PRECISION) {
054                                compare = -1;
055                        }
056                        if (compare == 0 && c2.getAmountFlavor() == AmountFlavor.PRECISION
057                                        && c1.getAmountFlavor() != AmountFlavor.PRECISION) {
058                                compare = 1;
059                        }
060                        if (compare == 0 && c1.getPrecision() == 0
061                                        && c2.getPrecision() != 0) {
062                                compare = -1;
063                        }
064                        if (compare == 0 && c2.getPrecision() == 0
065                                        && c1.getPrecision() != 0) {
066                                compare = 1;
067                        }
068                        if (compare == 0 && (c1.getMaxScale() > c2.getMaxScale())) {
069                                compare = -1;
070                        }
071                        if (compare == 0 && (c1.getMaxScale() < c2.getMaxScale())) {
072                                compare = 1;
073                        }
074                        return compare;
075                }
076        };
077
078        public DefaultMonetaryAmountsSpi() {
079                for (MonetaryAmountFactoryProviderSpi<?> f : Bootstrap
080                                .getServices(MonetaryAmountFactoryProviderSpi.class)) {
081                        MonetaryAmountFactoryProviderSpi<?> existing = factories.put(
082                                        f.getAmountType(), f);
083                        if (existing != null) {
084                                int compare = comparePriority(existing, f);
085                                if (compare < 0) {
086                                        Logger.getLogger(getClass().getName()).warning(
087                                                        "MonetaryAmountFactoryProviderSpi with lower prio ignored: "
088                                                                        + f);
089                                        factories.put(f.getAmountType(), existing);
090                                } else if (compare == 0) {
091                                        throw new IllegalStateException(
092                                                        "Ambigous MonetaryAmountFactoryProviderSpi found for "
093                                                                        + f.getAmountType() + ": "
094                                                                        + f.getClass().getName() + '/'
095                                                                        + existing.getClass().getName());
096                                }
097                        }
098                }
099        }
100
101        /**
102         * Comparator used for ordering the services provided.
103         * 
104         * @author Anatole Tresch
105         */
106        public static final class ProviderComparator implements Comparator<Object> {
107                @Override
108                public int compare(Object p1, Object p2) {
109                        return comparePriority(p1, p2);
110                }
111        }
112
113        /**
114         * Evaluates the service priority. Uses a {@link ServicePriority}, if
115         * present.
116         * 
117         * @param service
118         *            the service, not null.
119         * @return the priority from {@link ServicePriority}, or 0.
120         */
121        private static int getServicePriority(Object service) {
122                if (service == null) {
123                        return Integer.MIN_VALUE;
124                }
125                ServicePriority prio = service.getClass().getAnnotation(
126                                ServicePriority.class);
127                if (prio != null) {
128                        return prio.value();
129                }
130                return 0;
131        }
132
133        /**
134         * Compare two service priorities given the same service interface.
135         * 
136         * @param service1
137         *            first service, not null.
138         * @param service2
139         *            second service, not null.
140         * @param <T>
141         *            the interface type
142         * @return the comparison result.
143         */
144        public static <T> int comparePriority(T service1, T service2) {
145                return getServicePriority(service2) - getServicePriority(service1);
146        }
147
148        /**
149         * Tries to load the default {@link MonetaryAmount} class from
150         * {@code javamoney.properties} with contents as follows:<br/>
151         * <code>
152         * javax.money.defaults.amount.class=my.fully.qualified.ClassName
153         * </code>
154         * 
155         * @return the loaded default class, or {@code null}
156         */
157        // type check should be safe, exception will be logged if not.
158        @SuppressWarnings("unchecked")
159        private Class<? extends MonetaryAmount> loadDefaultAmountType() {
160                return null;
161        }
162
163
164        // save cast, since members are managed by this instance
165        @SuppressWarnings("unchecked")
166        @Override
167        public <T extends MonetaryAmount> MonetaryAmountFactory<T> getAmountFactory(
168                        Class<T> amountType) {
169                MonetaryAmountFactoryProviderSpi<T> f = MonetaryAmountFactoryProviderSpi.class
170                                .cast(factories.get(amountType));
171                if (f != null) {
172                        return f.createMonetaryAmountFactory();
173                }
174                throw new MonetaryException(
175                                "No matching MonetaryAmountFactory found, type="
176                                                + amountType.getName());
177        }
178
179        @Override
180        public Set<Class<? extends MonetaryAmount>> getAmountTypes() {
181                return factories.keySet();
182        }
183
184        /*
185         * (non-Javadoc)
186         * 
187         * @see javax.money.spi.MonetaryAmountsSpi#getDefaultAmountType()
188         */
189        @Override
190        public Class<? extends MonetaryAmount> getDefaultAmountType() {
191                if (configuredDefaultAmountType == null) {
192                        for (MonetaryAmountFactoryProviderSpi<?> f : Bootstrap
193                                        .getServices(MonetaryAmountFactoryProviderSpi.class)) {
194                                configuredDefaultAmountType = f.getAmountType();
195                                break;
196                        }
197                }
198                if (configuredDefaultAmountType == null) {
199                        throw new MonetaryException(
200                                        "No MonetaryAmountFactoryProviderSpi registered.");
201                }
202                return configuredDefaultAmountType;
203        }
204
205        /**
206         * (non-Javadoc)
207         * 
208         * @see javax.money.spi.MonetaryAmountsSpi#queryAmountType(javax.money.MonetaryContext)
209         */
210        @Override
211        public Class<? extends MonetaryAmount> queryAmountType(
212                        MonetaryContext requiredContext) {
213                if (requiredContext == null) {
214                        return getDefaultAmountType();
215                }
216                // first check for explicit type
217                for (@SuppressWarnings("unchecked")
218                MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f : Bootstrap
219                                .getServices(MonetaryAmountFactoryProviderSpi.class)) {
220                        if (f.getQueryInclusionPolicy() == QueryInclusionPolicy.NEVER) {
221                                continue;
222                        }
223                        if (requiredContext.getAmountType() == f.getAmountType()) {
224                                if (isPrecisionOK(requiredContext,
225                                                f.getMaximalMonetaryContext())) {
226                                        return f.getAmountType();
227                                } else {
228                                        throw new MonetaryException(
229                                                        "Incompatible context required=" + requiredContext
230                                                                        + ", maximal="
231                                                                        + f.getMaximalMonetaryContext());
232                                }
233                        }
234                }
235                // Select on required flavor
236                List<MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount>> selection = new ArrayList<>();
237                for (@SuppressWarnings("unchecked")
238                MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f : Bootstrap
239                                .getServices(MonetaryAmountFactoryProviderSpi.class)) {
240                        if (f.getDefaultMonetaryContext().getAmountFlavor() == AmountFlavor.UNDEFINED) {
241                                if (f.getQueryInclusionPolicy() == QueryInclusionPolicy.DIRECT_REFERENCE_ONLY
242                                                || f.getQueryInclusionPolicy() == QueryInclusionPolicy.NEVER) {
243                                        continue;
244                                }
245                                if (isPrecisionOK(requiredContext,
246                                                f.getMaximalMonetaryContext())) {
247                                        selection.add(f);
248                                }
249                        } else if (requiredContext.getAmountFlavor() == f
250                                        .getDefaultMonetaryContext().getAmountFlavor()) {
251                                if (isPrecisionOK(requiredContext,
252                                                f.getMaximalMonetaryContext())) {
253                                        selection.add(f);
254                                }
255                        }
256                }
257                if (selection.isEmpty()) {
258                        // fall back, add all selections, ignore flavor
259                        for (@SuppressWarnings("unchecked")
260                        MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f : Bootstrap
261                                        .getServices(MonetaryAmountFactoryProviderSpi.class)) {
262                                if (f.getQueryInclusionPolicy() == QueryInclusionPolicy.DIRECT_REFERENCE_ONLY
263                                                || f.getQueryInclusionPolicy() == QueryInclusionPolicy.NEVER) {
264                                        continue;
265                                }
266                                if (isPrecisionOK(requiredContext,
267                                                f.getMaximalMonetaryContext())) {
268                                        selection.add(f);
269                                }
270                        }
271                }
272                if (selection.size() == 1) {
273                        return selection.get(0).getAmountType();
274                } else {
275                        // several matches, check for required flavor
276                        for (@SuppressWarnings("unchecked")
277                        MonetaryAmountFactoryProviderSpi<? extends MonetaryAmount> f : selection) {
278                                if (f.getDefaultMonetaryContext().getAmountFlavor()
279                                                .equals(requiredContext.getAmountFlavor())) {
280                                        return f.getAmountType();
281                                }
282                        }
283                }
284                Collections.sort(selection, CONTEXT_COMPARATOR);
285                return selection.get(0).getAmountType();
286        }
287
288        private boolean isPrecisionOK(MonetaryContext requiredContext,
289                        MonetaryContext maximalMonetaryContext) {
290                if (maximalMonetaryContext.getPrecision() == 0) {
291                        return true;
292                }
293                if (requiredContext.getPrecision() == 0
294                                && maximalMonetaryContext.getPrecision() != 0) {
295                        return false;
296                }
297                if (requiredContext.getPrecision() > maximalMonetaryContext
298                                .getPrecision()) {
299                        return false;
300                }
301                if (requiredContext.getMaxScale() > maximalMonetaryContext
302                                .getMaxScale()) {
303                        return false;
304                }
305                return true;
306        }
307
308}