1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 
  33 package com.oracle.javafx.scenebuilder.kit.metadata.util;
  34 
  35 import java.io.File;
  36 import java.net.MalformedURLException;
  37 import java.net.URI;
  38 import java.net.URISyntaxException;
  39 import java.net.URL;
  40 import java.nio.file.Path;
  41 import java.nio.file.Paths;
  42 import java.util.MissingResourceException;
  43 import java.util.Objects;
  44 import java.util.ResourceBundle;
  45 import javafx.fxml.FXMLLoader;
  46 
  47 /**
  48  *
  49  */
  50 public class PrefixedValue {
  51 
  52     public enum Type {
  53         DOCUMENT_RELATIVE_PATH,
  54         CLASSLOADER_RELATIVE_PATH,
  55         RESOURCE_KEY,
  56         EXPRESSION,
  57         PLAIN_STRING,
  58         INVALID
  59     }
  60 
  61     private final String value;
  62     private final Type type;
  63 
  64     public PrefixedValue(String value) {
  65         assert value != null;
  66         this.value = value;
  67         this.type = getPrefixedValueType(this.value);
  68     }
  69 
  70     public PrefixedValue(Type type, String suffix) {
  71         assert type != Type.INVALID;
  72         assert suffix != null;
  73 
  74         this.type = type;
  75         switch(this.type) {
  76             case DOCUMENT_RELATIVE_PATH: {
  77                 final String encoding = encodePath(new File(suffix));
  78                 this.value = FXMLLoader.RELATIVE_PATH_PREFIX + encoding;
  79                 break;
  80             }
  81             case CLASSLOADER_RELATIVE_PATH: {
  82                 final String encoding = encodePath(new File(suffix));
  83                 this.value = FXMLLoader.RELATIVE_PATH_PREFIX + "/" + encoding; //NOI18N
  84                 break;
  85             }
  86             case RESOURCE_KEY: {
  87                 this.value = FXMLLoader.RESOURCE_KEY_PREFIX + suffix; //NOI18N
  88                 break;
  89             }
  90             case EXPRESSION: {
  91                 this.value = FXMLLoader.EXPRESSION_PREFIX + suffix; //NOI18N
  92                 break;
  93             }
  94             case PLAIN_STRING: {
  95                 if (suffix.startsWith(FXMLLoader.ESCAPE_PREFIX)
  96                         || suffix.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX)
  97                         || suffix.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX)
  98                         || suffix.startsWith(FXMLLoader.EXPRESSION_PREFIX)) {
  99                     this.value = FXMLLoader.ESCAPE_PREFIX + suffix;
 100                 } else {
 101                     this.value = suffix;
 102                 }
 103                 break;
 104             }
 105             default:
 106             case INVALID: {
 107                 // Emergency code
 108                 throw new IllegalArgumentException("Unexpected type " + Type.INVALID); //NOI18N
 109             }
 110         }
 111     }
 112 
 113     public Type getType() {
 114         return type;
 115     }
 116 
 117     public boolean isDocumentRelativePath() {
 118         return type == Type.DOCUMENT_RELATIVE_PATH;
 119     }
 120 
 121     public boolean isClassLoaderRelativePath() {
 122         return type == Type.CLASSLOADER_RELATIVE_PATH;
 123     }
 124 
 125     public boolean isResourceKey() {
 126         return type == Type.RESOURCE_KEY;
 127     }
 128 
 129     public boolean isExpression() {
 130         return type == Type.EXPRESSION;
 131     }
 132 
 133     public boolean isBindingExpression() {
 134         return isExpression() && getSuffix().startsWith("{"); //NOI18N
 135     }
 136 
 137     public boolean isPlainString() {
 138         return type == Type.PLAIN_STRING;
 139     }
 140 
 141     public boolean isInvalid() {
 142         return type == Type.INVALID;
 143     }
 144 
 145     public String getSuffix() {
 146         final String result;
 147 
 148         switch(this.type) {
 149             case DOCUMENT_RELATIVE_PATH: {
 150                 assert value.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX);
 151                 final String encoding = value.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length());
 152                 result = decodePath(encoding).getPath();
 153                 break;
 154             }
 155             case CLASSLOADER_RELATIVE_PATH: {
 156                 assert value.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX+"/"); //NOI18N
 157                 final String encoding = value.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length()+1);
 158                 result = decodePath(encoding).getPath();
 159                 break;
 160             }
 161             case RESOURCE_KEY: {
 162                 assert value.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX);
 163                 result = value.substring(FXMLLoader.RESOURCE_KEY_PREFIX.length());
 164                 break;
 165             }
 166             case EXPRESSION: {
 167                 assert value.startsWith(FXMLLoader.EXPRESSION_PREFIX);
 168                 result = value.substring(FXMLLoader.EXPRESSION_PREFIX.length());
 169                 break;
 170             }
 171             case PLAIN_STRING: {
 172                 if (value.startsWith(FXMLLoader.ESCAPE_PREFIX)) {
 173                     result = value.substring(FXMLLoader.ESCAPE_PREFIX.length());
 174                 } else if (value.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX+FXMLLoader.RELATIVE_PATH_PREFIX)) {
 175                     result = value.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length());
 176                 } else if (value.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX+FXMLLoader.RESOURCE_KEY_PREFIX)) {
 177                     result = value.substring(FXMLLoader.RESOURCE_KEY_PREFIX.length());
 178                 } else if (value.startsWith(FXMLLoader.EXPRESSION_PREFIX+FXMLLoader.EXPRESSION_PREFIX)) {
 179                     result = value.substring(FXMLLoader.EXPRESSION_PREFIX.length());
 180                 } else {
 181                     result = value;
 182                 }
 183                 break;
 184             }
 185             default:
 186             case INVALID: {
 187                 // Emergency code
 188                 result = null;
 189             }
 190         }
 191 
 192         return result;
 193     }
 194 
 195     public URL resolveDocumentRelativePath(URL document) {
 196         assert document != null;
 197         assert type == Type.DOCUMENT_RELATIVE_PATH;
 198 
 199         URL result;
 200         try {
 201             final String path = value.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length());
 202             result = new URL(document, path);
 203         } catch(MalformedURLException x) {
 204             result = null;
 205         }
 206 
 207         return result;
 208     }
 209 
 210     public URL resolveClassLoaderRelativePath(ClassLoader classLoader) {
 211         assert classLoader != null;
 212         assert type == Type.CLASSLOADER_RELATIVE_PATH;
 213         assert value.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX+"/");
 214 
 215         final String path = value.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length()+1);
 216         return classLoader.getResource(path);
 217     }
 218 
 219     public String resolveResourceKey(ResourceBundle resources) {
 220         assert resources != null;
 221         assert type == Type.RESOURCE_KEY;
 222         assert value.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX);
 223 
 224         String result;
 225         try {
 226             final String key = value.substring(FXMLLoader.RESOURCE_KEY_PREFIX.length());
 227             result = resources.getString(key);
 228         } catch(MissingResourceException x) {
 229             result = null;
 230         }
 231 
 232         return result;
 233     }
 234 
 235     public static Type getPrefixedValueType(String prefixedValue) {
 236         final Type result;
 237 
 238         String v = prefixedValue;
 239         if (v.startsWith(FXMLLoader.ESCAPE_PREFIX)) {
 240             v = v.substring(FXMLLoader.ESCAPE_PREFIX.length());
 241             if (v.isEmpty()
 242                 || !(v.startsWith(FXMLLoader.ESCAPE_PREFIX)
 243                     || v.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX)
 244                     || v.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX)
 245                     || v.startsWith(FXMLLoader.EXPRESSION_PREFIX)
 246                     || v.startsWith(FXMLLoader.BI_DIRECTIONAL_BINDING_PREFIX))) {
 247                 result = Type.INVALID;
 248             } else {
 249                 result = Type.PLAIN_STRING;
 250             }
 251         } else if (v.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX)) {
 252             v = v.substring(FXMLLoader.RELATIVE_PATH_PREFIX.length());
 253             if (v.isEmpty()) {
 254                 result = Type.INVALID;
 255             } else if (v.startsWith(FXMLLoader.RELATIVE_PATH_PREFIX)) {
 256                 // The prefix was escaped
 257                 result = Type.PLAIN_STRING;
 258             } else if (v.charAt(0) == '/') {
 259                 result = Type.CLASSLOADER_RELATIVE_PATH;
 260             } else {
 261                 result = Type.DOCUMENT_RELATIVE_PATH;
 262             }
 263         } else if (v.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX)) {
 264             v = v.substring(FXMLLoader.RESOURCE_KEY_PREFIX.length());
 265             if (v.isEmpty()) {
 266                 result = Type.INVALID;
 267             } else if (v.startsWith(FXMLLoader.RESOURCE_KEY_PREFIX)) {
 268                 // The prefix was escaped
 269                 result = Type.PLAIN_STRING;
 270             } else {
 271                 result = Type.RESOURCE_KEY;
 272             }
 273         } else if (v.startsWith(FXMLLoader.EXPRESSION_PREFIX)) {
 274             v = v.substring(FXMLLoader.EXPRESSION_PREFIX.length());
 275             if (v.isEmpty()) {
 276                 result = Type.INVALID;
 277             } else if (v.startsWith(FXMLLoader.EXPRESSION_PREFIX)) {
 278                 // The prefix was escaped
 279                 result = Type.PLAIN_STRING;
 280             } else {
 281                 result = Type.EXPRESSION;
 282             }
 283         } else {
 284             result = Type.PLAIN_STRING;
 285         }
 286 
 287         return result;
 288     }
 289 
 290     public static PrefixedValue makePrefixedValue(URL assetURL, URL documentURL) {
 291 
 292         final File assetFile, documentFile;
 293         try {
 294             assetFile = new File(assetURL.toURI());
 295             documentFile = new File(documentURL.toURI());
 296         } catch(URISyntaxException x) {
 297             throw new IllegalArgumentException(x);
 298         }
 299         final File parentFile = documentFile.getParentFile();
 300 
 301         final PrefixedValue result;
 302         if ((parentFile == null) || parentFile.equals(assetFile)) {
 303             throw new IllegalArgumentException(documentURL.toString());
 304         } else {
 305             final Path relativePath = parentFile.toPath().relativize(assetFile.toPath());
 306             result = new PrefixedValue(Type.DOCUMENT_RELATIVE_PATH, relativePath.toString());
 307         }
 308 
 309         return result;
 310     }
 311 
 312 
 313     /*
 314      * Object
 315      */
 316 
 317     @Override
 318     public String toString() {
 319         return value;
 320     }
 321 
 322     @Override
 323     public int hashCode() {
 324         int hash = 5;
 325         hash = 97 * hash + Objects.hashCode(getSuffix());
 326         hash = 97 * hash + Objects.hashCode(this.type);
 327         return hash;
 328     }
 329 
 330     @Override
 331     public boolean equals(Object obj) {
 332         if (obj == null) {
 333             return false;
 334         }
 335         if (getClass() != obj.getClass()) {
 336             return false;
 337         }
 338         final PrefixedValue other = (PrefixedValue) obj;
 339         if (!Objects.equals(this.getSuffix(), other.getSuffix())) {
 340             return false;
 341         }
 342         if (this.type != other.type) {
 343             return false;
 344         }
 345         return true;
 346     }
 347 
 348 
 349     /*
 350      * Private
 351      */
 352 
 353     private static String encodePath(File file) {
 354         final String result;
 355 
 356         try {
 357             if (file.isAbsolute()) {
 358                 result = file.toURI().toURL().getPath();
 359             } else {
 360                 final Path tmpPath = Paths.get(System.getProperty("java.io.tmpdir")); //NOI18N
 361                 final String tmpPathEncoding = tmpPath.toFile().toURI().toURL().getPath();
 362                 final Path absolutePath = tmpPath.resolve(file.toPath());
 363                 final String absoluteEncoding = absolutePath.toFile().toURI().toURL().getPath();
 364                 assert absoluteEncoding.startsWith(tmpPathEncoding);
 365                 result = absoluteEncoding.substring(tmpPathEncoding.length());
 366             }
 367         } catch(MalformedURLException x) {
 368             throw new IllegalStateException(x);
 369         }
 370 
 371         return result;
 372     }
 373 
 374     private static File decodePath(String encoding) {
 375         File result;
 376 
 377         try {
 378             if (encoding.startsWith("/")) { //NOI18N
 379                 result = new File(new URI("file:" + encoding)) ; //NOI18N
 380             } else {
 381                 final Path tmpPath = Paths.get(System.getProperty("java.io.tmpdir")); //NOI18N
 382                 final URL tmpPathURL = tmpPath.toFile().toURI().toURL();
 383                 final URL absoluteURL = new URL(tmpPathURL.toString() + "/" + encoding); //NOI18N
 384                 final File absoluteFile = new File(absoluteURL.toURI());
 385                 final Path absolutePath = absoluteFile.toPath();
 386                 assert absolutePath.startsWith(tmpPath);
 387                 final Path relativePath = tmpPath.relativize(absolutePath);
 388                 result = relativePath.toFile();
 389             }
 390 
 391             assert encoding.equals(encodePath(result));
 392 
 393         } catch(MalformedURLException | URISyntaxException x) {
 394             result = new File(encoding);
 395         }
 396 
 397 
 398         return result;
 399     }
 400 }