1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.metapath.function;
7   
8   import java.util.HashMap;
9   import java.util.Map;
10  import java.util.concurrent.locks.Lock;
11  import java.util.concurrent.locks.ReadWriteLock;
12  import java.util.concurrent.locks.ReentrantReadWriteLock;
13  import java.util.stream.Stream;
14  
15  import dev.metaschema.core.qname.IEnhancedQName;
16  import dev.metaschema.core.util.ObjectUtils;
17  import edu.umd.cs.findbugs.annotations.NonNull;
18  import edu.umd.cs.findbugs.annotations.Nullable;
19  
20  /**
21   * Provides a catalog of registered function implementations.
22   * <p>
23   * Functions can be registered using the {@link #registerFunction(IFunction)}
24   * method.
25   * <p>
26   * Previously registered functions can be looked up using the
27   * {@link #getFunction(IEnhancedQName, int)} method.
28   */
29  public class FunctionLibrary implements IFunctionLibrary {
30  
31    @NonNull
32    private final Map<IEnhancedQName, NamedFunctionSet> libraryByQName = new HashMap<>(); // NOPMD - intentional
33    @NonNull
34    private final ReadWriteLock instanceLock = new ReentrantReadWriteLock();
35  
36    /**
37     * Register the provided function signature.
38     *
39     * @param function
40     *          the function signature to register
41     * @throws IllegalArgumentException
42     *           if the provided function has the same arity as a previously
43     *           registered function with the same name
44     */
45    public final void registerFunction(@NonNull IFunction function) {
46      registerFunctionByQName(function);
47    }
48  
49    private void registerFunctionByQName(@NonNull IFunction function) {
50      IEnhancedQName qname = function.getQName();
51      IFunction duplicate;
52      Lock writeLock = instanceLock.writeLock();
53      writeLock.lock();
54      try {
55        NamedFunctionSet functions = libraryByQName.get(qname);
56        if (functions == null) {
57          functions = new NamedFunctionSet();
58          libraryByQName.put(qname, functions);
59        }
60        duplicate = functions.addFunction(function);
61      } finally {
62        writeLock.unlock();
63      }
64      if (duplicate != null) {
65        throw new IllegalArgumentException(String.format("Duplicate functions with same arity: %s shadows %s",
66            duplicate.toSignature(), function.toSignature()));
67      }
68    }
69  
70    @Override
71    public Stream<IFunction> stream() {
72      Lock readLock = instanceLock.readLock();
73      readLock.lock();
74      try {
75        return ObjectUtils.notNull(libraryByQName.values().stream().flatMap(NamedFunctionSet::getFunctionsAsStream));
76      } finally {
77        readLock.unlock();
78      }
79    }
80  
81    @Override
82    public IFunction getFunction(@NonNull IEnhancedQName name, int arity) {
83      IFunction retval = null;
84      Lock readLock = instanceLock.readLock();
85      readLock.lock();
86      try {
87        NamedFunctionSet functions = libraryByQName.get(name);
88        if (functions != null) {
89          retval = functions.getFunctionWithArity(arity);
90        }
91      } finally {
92        readLock.unlock();
93      }
94      return retval;
95    }
96  
97    private static class NamedFunctionSet {
98      private final Map<Integer, IFunction> arityToFunctionMap;
99      private IFunction unboundedArity;
100 
101     public NamedFunctionSet() {
102       this.arityToFunctionMap = new HashMap<>();
103     }
104 
105     @SuppressWarnings("null")
106     @NonNull
107     public Stream<IFunction> getFunctionsAsStream() {
108       return arityToFunctionMap.values().stream();
109     }
110 
111     @Nullable
112     public IFunction getFunctionWithArity(int arity) {
113       IFunction retval = arityToFunctionMap.get(arity);
114       if (retval == null && unboundedArity != null && unboundedArity.arity() < arity) {
115         retval = unboundedArity;
116       }
117       return retval;
118     }
119 
120     @Nullable
121     public IFunction addFunction(@NonNull IFunction function) {
122       IFunction old = arityToFunctionMap.put(function.arity(), function);
123       if (function.isArityUnbounded()) {
124         unboundedArity = function;
125       }
126       return old;
127     }
128   }
129 }