2020import java .io .IOException ;
2121import java .io .InputStream ;
2222import java .lang .reflect .InvocationTargetException ;
23+ import java .net .UnknownServiceException ;
24+ import java .nio .file .Path ;
25+ import java .security .Security ;
26+ import java .util .Arrays ;
27+ import java .util .Objects ;
28+ import java .util .Optional ;
2329import java .util .Properties ;
30+ import java .util .Set ;
2431import java .util .concurrent .ConcurrentHashMap ;
2532import java .util .concurrent .ConcurrentMap ;
33+ import java .util .stream .Collectors ;
2634
2735/**
2836 *
2937 */
30- public class FactoryFinder {
38+ public class FactoryFinder < T > {
3139
3240 /**
3341 * The strategy that the FactoryFinder uses to find load and instantiate Objects
@@ -44,51 +52,54 @@ public interface ObjectFactory {
4452 * @param path the full service path
4553 * @return
4654 */
47- public Object create (String path ) throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException ;
55+ Object create (String path ) throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException ;
4856
57+ @ SuppressWarnings ("unchecked" )
58+ default <T > T create (String path , Class <T > requiredType , Set <Class <? extends T >> allowedImpls )
59+ throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException {
60+ return (T ) create (path );
61+ }
4962 }
5063
5164 /**
5265 * The default implementation of Object factory which works well in standalone applications.
5366 */
5467 protected static class StandaloneObjectFactory implements ObjectFactory {
55- final ConcurrentMap <String , Class > classMap = new ConcurrentHashMap <String , Class >();
68+ final ConcurrentMap <String , Class <?>> classMap = new ConcurrentHashMap <>();
5669
5770 @ Override
58- public Object create (final String path ) throws InstantiationException , IllegalAccessException , ClassNotFoundException , IOException {
59- Class clazz = classMap .get (path );
71+ public Object create (final String path )
72+ throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException {
73+ throw new UnsupportedOperationException ("Create is not supported without requiredType and allowed impls" );
74+ }
75+
76+ @ SuppressWarnings ("unchecked" )
77+ @ Override
78+ public <T > T create (String path , Class <T > requiredType , Set <Class <? extends T >> allowedImpls ) throws InstantiationException , IllegalAccessException , ClassNotFoundException , IOException {
79+ Class <?> clazz = classMap .get (path );
6080 if (clazz == null ) {
6181 clazz = loadClass (loadProperties (path ));
82+ // no reason to cache if invalid so validate before caching
83+ validateClass (clazz , requiredType , allowedImpls );
6284 classMap .put (path , clazz );
85+ } else {
86+ // Validate again (even for previously cached classes) in case
87+ // a path is re-used with a different requiredType.
88+ // This object factory is static and shared by all factory finder instances by default,
89+ // so it would be possible (although probably a mistake) to use the same
90+ // path again with a different requiredType in a different FactoryFinder
91+ validateClass (clazz , requiredType , allowedImpls );
6392 }
64-
93+
6594 try {
66- return clazz .getConstructor ().newInstance ();
95+ return ( T ) clazz .getConstructor ().newInstance ();
6796 } catch (NoSuchMethodException | InvocationTargetException e ) {
6897 throw new InstantiationException (e .getMessage ());
6998 }
7099 }
71100
72- static public Class loadClass (Properties properties ) throws ClassNotFoundException , IOException {
73-
74- String className = properties .getProperty ("class" );
75- if (className == null ) {
76- throw new IOException ("Expected property is missing: class" );
77- }
78- Class clazz = null ;
79- ClassLoader loader = Thread .currentThread ().getContextClassLoader ();
80- if (loader != null ) {
81- try {
82- clazz = loader .loadClass (className );
83- } catch (ClassNotFoundException e ) {
84- // ignore
85- }
86- }
87- if (clazz == null ) {
88- clazz = FactoryFinder .class .getClassLoader ().loadClass (className );
89- }
90-
91- return clazz ;
101+ static Class <?> loadClass (Properties properties ) throws ClassNotFoundException , IOException {
102+ return FactoryFinder .loadClass (properties .getProperty ("class" ));
92103 }
93104
94105 static public Properties loadProperties (String uri ) throws IOException {
@@ -138,21 +149,114 @@ public static void setObjectFactory(ObjectFactory objectFactory) {
138149 // Instance methods and properties
139150 // ================================================================
140151 private final String path ;
152+ private final Class <T > requiredType ;
153+ private final Set <Class <? extends T >> allowedImpls ;
154+
155+ public FactoryFinder (String path , Class <T > requiredType ) {
156+ this (path , requiredType , null );
157+ }
141158
142- public FactoryFinder (String path ) {
143- this .path = path ;
159+ public FactoryFinder (String path , Class <T > requiredType , String allowedImpls ) {
160+ this .path = Objects .requireNonNull (path );
161+ this .requiredType = Objects .requireNonNull (requiredType );
162+ this .allowedImpls = loadAllowedImpls (requiredType , allowedImpls );
144163 }
145164
165+ @ SuppressWarnings ("unchecked" )
166+ private static <T > Set <Class <? extends T >> loadAllowedImpls (Class <T > requiredType , String allowedImpls ) {
167+ // If allowedImpls is either null or an asterisk (allow all wild card) then set to null so we don't filter
168+ // If allowedImpls is only an empty string we return an empty set meaning allow none
169+ // Otherwise split/trim all values
170+ return allowedImpls != null && !allowedImpls .equals ("*" ) ?
171+ Arrays .stream (allowedImpls .split ("\\ s*,\\ s*" ))
172+ .filter (s -> !s .isEmpty ())
173+ .map (s -> {
174+ try {
175+ final Class <?> clazz = FactoryFinder .loadClass (s );
176+ if (!requiredType .isAssignableFrom (clazz )) {
177+ throw new IllegalArgumentException (
178+ "Class " + clazz + " is not assignable to " + requiredType );
179+ }
180+ return (Class <? extends T >)clazz ;
181+ } catch (ClassNotFoundException | IOException e ) {
182+ throw new IllegalArgumentException (e );
183+ }
184+ }).collect (Collectors .toUnmodifiableSet ()) : null ;
185+ }
186+
187+
146188 /**
147189 * Creates a new instance of the given key
148190 *
149191 * @param key is the key to add to the path to find a text file containing
150192 * the factory name
151193 * @return a newly created instance
152194 */
153- public Object newInstance (String key ) throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException {
154- return objectFactory .create (path +key );
195+ public T newInstance (String key ) throws IllegalAccessException , InstantiationException , IOException , ClassNotFoundException {
196+ return objectFactory .create (resolvePath (key ), requiredType , allowedImpls );
197+ }
198+
199+ Set <Class <? extends T >> getAllowedImpls () {
200+ return allowedImpls ;
155201 }
156202
203+ Class <T > getRequiredType () {
204+ return requiredType ;
205+ }
206+
207+ private String resolvePath (final String key ) throws InstantiationException {
208+ // Normalize the base path with the given key. This
209+ // will resolve/remove any relative ".." sections of the path.
210+ // Example: "/dir1/dir2/dir3/../file" becomes "/dir1/dir2/file"
211+ final Path resolvedPath = Path .of (path ).resolve (key ).normalize ();
212+
213+ // Validate the resoled path is still within the original defined
214+ // root path and throw an error of it is not.
215+ if (!resolvedPath .startsWith (path )) {
216+ throw new InstantiationException ("Provided key escapes the FactoryFinder configured directory" );
217+ }
218+
219+ return resolvedPath .toString ();
220+ }
221+
222+ public static String buildAllowedImpls (Class <?>...classes ) {
223+ return Arrays .stream (Objects .requireNonNull (classes , "List of allowed classes may not be null" ))
224+ .map (Class ::getName ).collect (Collectors .joining ("," ));
225+ }
226+
227+ public static <T > void validateClass (Class <?> clazz , Class <T > requiredType ,
228+ Set <Class <? extends T >> allowedImpls ) throws InstantiationException {
229+ // Validate the loaded class is an allowed impl
230+ if (allowedImpls != null && !allowedImpls .contains (clazz )) {
231+ throw new InstantiationException ("Class " + clazz + " is not an allowed implementation "
232+ + "of type " + requiredType );
233+ }
234+ // Validate the loaded class is a subclass of the right type
235+ // The allowedImpls may not be used so also check requiredType. Even if set
236+ // generics can be erased and this is an extra safety check
237+ if (!requiredType .isAssignableFrom (clazz )) {
238+ throw new InstantiationException ("Class " + clazz + " is not assignable to " + requiredType );
239+ }
240+ }
241+
242+ static Class <?> loadClass (String className ) throws ClassNotFoundException , IOException {
243+ if (className == null ) {
244+ throw new IOException ("Expected property is missing: class" );
245+ }
246+ Class <?> clazz = null ;
247+ ClassLoader loader = Thread .currentThread ().getContextClassLoader ();
248+ if (loader != null ) {
249+ try {
250+ clazz = loader .loadClass (className );
251+ } catch (ClassNotFoundException e ) {
252+ // ignore
253+ }
254+ }
255+ if (clazz == null ) {
256+ clazz = FactoryFinder .class .getClassLoader ().loadClass (className );
257+ }
258+
259+ return clazz ;
260+ }
157261
158262}
0 commit comments