6767import java .io .File ;
6868import java .io .FileOutputStream ;
6969import java .io .IOException ;
70+ import java .io .InputStream ;
7071import java .io .Writer ;
7172import java .lang .reflect .Field ;
7273import java .lang .reflect .InvocationTargetException ;
74+ import java .net .MalformedURLException ;
75+ import java .net .URL ;
76+ import java .nio .file .Path ;
7377import java .util .ArrayList ;
7478import java .util .Comparator ;
7579import java .util .Deque ;
@@ -299,6 +303,8 @@ void declareAttributeList(String tag, Class<?> type, Writer writer) throws IOExc
299303 private String name ;
300304 private ResourceBundle resourceBundle ;
301305
306+ private Path path ;
307+
302308 private Map <String , Field > fields = new HashMap <>();
303309 private Map <String , ButtonGroup > groups = new HashMap <>();
304310
@@ -586,16 +592,20 @@ private UILoader(Object owner, String name, ResourceBundle resourceBundle) {
586592 this .resourceBundle = resourceBundle ;
587593 }
588594
589- private JComponent load () {
590- var type = owner .getClass ();
595+ private UILoader (Path path ) {
596+ this .path = path ;
597+ }
591598
592- var fields = type .getDeclaredFields ();
599+ private JComponent load () {
600+ if (owner != null ) {
601+ var fields = owner .getClass ().getDeclaredFields ();
593602
594- for (var i = 0 ; i < fields .length ; i ++) {
595- var field = fields [i ];
603+ for (var i = 0 ; i < fields .length ; i ++) {
604+ var field = fields [i ];
596605
597- if (JComponent .class .isAssignableFrom (field .getType ())) {
598- this .fields .put (field .getName (), field );
606+ if (JComponent .class .isAssignableFrom (field .getType ())) {
607+ this .fields .put (field .getName (), field );
608+ }
599609 }
600610 }
601611
@@ -605,7 +615,7 @@ private JComponent load() {
605615 xmlInputFactory .setProperty ("javax.xml.stream.isSupportingExternalEntities" , false );
606616 xmlInputFactory .setProperty ("javax.xml.stream.supportDTD" , false );
607617
608- try (var inputStream = type . getResourceAsStream ( name )) {
618+ try (var inputStream = open ( )) {
609619 if (inputStream == null ) {
610620 throw new UnsupportedOperationException ("Named resource does not exist." );
611621 }
@@ -631,6 +641,14 @@ private JComponent load() {
631641 return root ;
632642 }
633643
644+ private InputStream open () throws IOException {
645+ if (owner != null ) {
646+ return owner .getClass ().getResourceAsStream (name );
647+ } else {
648+ return path .toUri ().toURL ().openStream ();
649+ }
650+ }
651+
634652 private void processStartElement (XMLStreamReader xmlStreamReader ) {
635653 var tag = xmlStreamReader .getLocalName ();
636654
@@ -660,22 +678,24 @@ private void processStartElement(XMLStreamReader xmlStreamReader) {
660678 if (name .equals (NAME )) {
661679 component .setName (value );
662680
663- var field = fields .get (value );
681+ if (owner != null ) {
682+ var field = fields .get (value );
664683
665- if (field == null ) {
666- throw new UnsupportedOperationException (String .format ("Invalid field name (%s)." , value ));
667- }
684+ if (field == null ) {
685+ throw new UnsupportedOperationException (String .format ("Invalid field name (%s)." , value ));
686+ }
668687
669- field .setAccessible (true );
688+ field .setAccessible (true );
670689
671- try {
672- if (field .get (owner ) != null ) {
673- throw new UnsupportedOperationException (String .format ("Field is already assigned (%s)." , value ));
674- }
690+ try {
691+ if (field .get (owner ) != null ) {
692+ throw new UnsupportedOperationException (String .format ("Field is already assigned (%s)." , value ));
693+ }
675694
676- field .set (owner , component );
677- } catch (IllegalAccessException exception ) {
678- throw new UnsupportedOperationException (exception );
695+ field .set (owner , component );
696+ } catch (IllegalAccessException exception ) {
697+ throw new UnsupportedOperationException (exception );
698+ }
679699 }
680700 } else if (name .equals (GROUP )) {
681701 if (!(component instanceof AbstractButton button )) {
@@ -797,25 +817,33 @@ private String getText(String value) {
797817 }
798818
799819 private Icon getIcon (String value ) {
800- return icons .computeIfAbsent (value , key -> {
801- if (value .endsWith (".svg" )) {
802- return new FlatSVGIcon (owner .getClass ().getResource (value ));
803- } else {
804- throw new UnsupportedOperationException ("Unsupported icon type." );
805- }
806- });
820+ return icons .computeIfAbsent (value , key -> new FlatSVGIcon (getURL (value )));
807821 }
808822
809823 private Image getImage (String value ) {
810824 return images .computeIfAbsent (value , key -> {
811825 try {
812- return ImageIO .read (owner . getClass (). getResource (value ));
826+ return ImageIO .read (getURL (value ));
813827 } catch (IOException exception ) {
814- throw new UnsupportedOperationException (exception );
828+ throw new RuntimeException (exception );
815829 }
816830 });
817831 }
818832
833+ private URL getURL (String value ) {
834+ if (owner != null ) {
835+ return owner .getClass ().getResource (value );
836+ } else {
837+ var uri = path .getParent ().resolve (value ).toUri ();
838+
839+ try {
840+ return uri .toURL ();
841+ } catch (MalformedURLException exception ) {
842+ throw new RuntimeException (exception );
843+ }
844+ }
845+ }
846+
819847 private void processEndElement () {
820848 root = components .pop ();
821849 }
@@ -861,6 +889,25 @@ public static JComponent load(Object owner, String name, ResourceBundle resource
861889 return uiLoader .load ();
862890 }
863891
892+ /**
893+ * Deserializes a component hierarchy from a markup document.
894+ *
895+ * @param path
896+ * The document's path.
897+ *
898+ * @return
899+ * The deserialized component hierarchy.
900+ */
901+ public static JComponent load (Path path ) {
902+ if (path == null ) {
903+ throw new IllegalArgumentException ();
904+ }
905+
906+ var uiLoader = new UILoader (path );
907+
908+ return uiLoader .load ();
909+ }
910+
864911 /**
865912 * Associates a markup tag with a component type.
866913 *
0 commit comments