View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.model;
35  
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  /**
40   * Module dependencies can use a range syntax for versions.
41   * Supports the following syntaxes
42   * <ul>
43   * <li>* - includes all versions</li>
44   * <li>1.2 - matches only 1.2 exactly </li>
45   * <li>1.2/* - matches all versions including and above 1.2</li>
46   * <li>1.2/1.2.9 - matches all versions including and between 1.2 and 1.2.9</li>
47   * <li>[1.2,1.2.9] - inward square brackets mean inclusion</li>
48   * <li>[1.2,1.2.9[ - outward square brackets mean exclusion</li>
49   * <li>[1.2,1.2.9) - parenthesis mean exclusion</li>
50   * </ul>
51   *
52   * @see info.magnolia.module.model.VersionRangeTest for examples.
53   */
54  public class VersionRange {
55      // a bracket, a version, a comma, a version, a bracket
56      private final static Pattern boundedRangeSyntax = Pattern.compile("^([(\\[\\]])(.*),(.*)([)\\[\\]])$");
57      // make sure we start and end with characters that are not brackets
58      private final static Pattern notBoundedByBrackets = Pattern.compile("^([^(\\[\\]])(.*)([^)\\[\\]])$", Pattern.DOTALL);
59      private final Version from;
60      private final Version to;
61      private final Boundary leftBoundary;
62      private final Boundary rightBoundary;
63  
64      public static VersionRange parse(String input) {
65          return new VersionRange(input);
66      }
67  
68      /**
69       * @deprecated since 5.0.2 use {@link VersionRange#parse(String)} instead - should not be public.
70       */
71      public VersionRange(String input) {
72          final String rangeDef = input == null ? "*" : input;
73          final int slashIdx = rangeDef.indexOf('/');
74          final int commaIdx = rangeDef.indexOf(',');
75          final Matcher boundedRangeMatcher = input != null ? boundedRangeSyntax.matcher(input) : null;
76          final Matcher notBoundedByBracketsMatcher = input != null ? notBoundedByBrackets.matcher(input) : null;
77          if (slashIdx < 0 && commaIdx < 0) {
78              this.from = newVersionOrStar(rangeDef, Version.UNDEFINED_FROM);
79              this.to = newVersionOrStar(rangeDef, Version.UNDEFINED_TO);
80              this.leftBoundary = Boundary.leftInclSq;
81              this.rightBoundary = Boundary.rightInclSq;
82          } else if (slashIdx >= 0 && notBoundedByBracketsMatcher != null && notBoundedByBracketsMatcher.matches()) {
83              this.from = newVersionOrStar(rangeDef.substring(0, slashIdx), Version.UNDEFINED_FROM);
84              this.to = newVersionOrStar(rangeDef.substring(slashIdx + 1), Version.UNDEFINED_TO);
85              this.leftBoundary = Boundary.leftInclSq;
86              this.rightBoundary = Boundary.rightInclSq;
87          } else if (boundedRangeMatcher != null && boundedRangeMatcher.matches()) {
88              this.leftBoundary = Boundary.bySymbol(boundedRangeMatcher.group(1).charAt(0), Hand.left);
89              this.from = newVersionOrStarOrEmpty(boundedRangeMatcher.group(2), Version.UNDEFINED_FROM);
90              this.to = newVersionOrStarOrEmpty(boundedRangeMatcher.group(3), Version.UNDEFINED_TO);
91              this.rightBoundary = Boundary.bySymbol(boundedRangeMatcher.group(4).charAt(0), Hand.right);
92          } else {
93              throw new IllegalArgumentException("Can't parse version range: " + input);
94          }
95          validate(input);
96      }
97  
98      /**
99       * @deprecated should not be public - since 5.0.2
100      */
101     public VersionRange(Version from, Version to) {
102         this(from, Boundary.leftInclSq, to, Boundary.rightInclSq);
103     }
104 
105     /**
106      * @deprecated should not be public - since 5.0.2
107      */
108     private VersionRange(Version from, Boundary leftBoundary, Version to, Boundary rightBoundary) {
109         this.from = from;
110         this.to = to;
111         this.leftBoundary = leftBoundary;
112         this.rightBoundary = rightBoundary;
113         validate("from:" + from + ",to:" + to + ",leftBoundary:" + leftBoundary + ",rightBoundary:" + rightBoundary);
114     }
115 
116     private Version newVersionOrStarOrEmpty(String rangeDef, Version ifUndefined) {
117         if ("".equals(rangeDef.trim())) {
118             return ifUndefined;
119         }
120         return newVersionOrStar(rangeDef, ifUndefined);
121     }
122 
123     private Version newVersionOrStar(String rangeDef, Version ifUndefined) {
124         if ("*".equals(rangeDef.trim())) {
125             return ifUndefined;
126         }
127         return Version.parseVersion(rangeDef);
128     }
129 
130     /**
131      * @param inputSpec the range as specified by the caller. Should not be used for actual validation, but can be used for error messages.
132      */
133     private void validate(String inputSpec) {
134         if (from.isStrictlyAfter(to)) {
135             throw new IllegalArgumentException("Invalid range: " + from + " is not after " + to + " (specified as \"" + inputSpec + "\")");
136         }
137     }
138 
139     /**
140      * @deprecated should not be public - since 5.0.2
141      */
142     public Version getFrom() {
143         return from;
144     }
145 
146     /**
147      * @deprecated should not be public - since 5.0.2
148      */
149     public Version getTo() {
150         return to;
151     }
152 
153     public boolean contains(Version other) {
154         if (other.isEquivalent(from) && leftBoundary.include) {
155             return true;
156         }
157         if (other.isEquivalent(to) && rightBoundary.include) {
158             return true;
159         }
160         return other.isStrictlyAfter(from) && to.isStrictlyAfter(other);
161     }
162 
163     @Override
164     public String toString() {
165         final StringBuilder sb = new StringBuilder();
166         if (from.equals(Version.UNDEFINED_FROM)) {
167             sb.append(Boundary.leftParen.symbol);
168         } else {
169             sb.append(leftBoundary.symbol);
170             sb.append(from);
171         }
172         sb.append(',');
173         if (to.equals(Version.UNDEFINED_TO)) {
174             sb.append(Boundary.rightParen.symbol);
175         } else {
176             sb.append(to);
177             sb.append(rightBoundary.symbol);
178         }
179         return sb.toString();
180     }
181 
182     private static enum Hand {left, right}
183 
184     private static enum Boundary {
185         leftExclSq(Hand.left, ']', false), leftInclSq(Hand.left, '[', true), leftParen(Hand.left, '(', false),
186         rightExclSq(Hand.right, '[', false), rightInclSq(Hand.right, ']', true), rightParen(Hand.right, ')', false);
187 
188         private final Hand hand;
189         private final char symbol;
190         private final boolean include;
191 
192         private Boundary(Hand hand, char symbol, boolean include) {
193             this.hand = hand;
194             this.symbol = symbol;
195             this.include = include;
196         }
197 
198         static Boundary bySymbol(char c, Hand h) {
199             for (Boundary boundary : values()) {
200                 if (boundary.hand == h && boundary.symbol == c) {
201                     return boundary;
202                 }
203             }
204             throw new IllegalArgumentException("No " + Boundary.class.getSimpleName() + " found by symbol " + c + " for " + h.name() + " boundary. ");
205         }
206     }
207 
208 }