View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.model.types;
17  
18  import org.fourthline.cling.model.Constants;
19  
20  import java.util.logging.Logger;
21  import java.util.regex.Pattern;
22  import java.util.regex.Matcher;
23  
24  /**
25   * Represents a service type, for example <code>urn:my-domain-namespace:service:MyService:1</code>.
26   * <p>
27   * Although decimal versions are accepted and parsed, the version used for
28   * comparison is only the integer withou the fraction.
29   * </p>
30   *
31   * @author Christian Bauer
32   */
33  public class ServiceType {
34  
35      final private static Logger log = Logger.getLogger(ServiceType.class.getName());
36  
37      public static final Pattern PATTERN =
38          Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):service:(" + Constants.REGEX_TYPE + "):([0-9]+).*");
39  
40      // Note: 'serviceId' vs. 'service'
41      public static final Pattern BROKEN_PATTERN =
42          Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):serviceId:(" + Constants.REGEX_TYPE + "):([0-9]+).*");
43  
44      private String namespace;
45      private String type;
46      private int version = 1;
47  
48      public ServiceType(String namespace, String type) {
49          this(namespace, type, 1);
50      }
51  
52      public ServiceType(String namespace, String type, int version) {
53  
54          if (namespace != null && !namespace.matches(Constants.REGEX_NAMESPACE)) {
55              throw new IllegalArgumentException("Service type namespace contains illegal characters");
56          }
57          this.namespace = namespace;
58  
59          if (type != null && !type.matches(Constants.REGEX_TYPE)) {
60              throw new IllegalArgumentException("Service type suffix too long (64) or contains illegal characters");
61          }
62          this.type = type;
63  
64          this.version = version;
65      }
66  
67      public String getNamespace() {
68          return namespace;
69      }
70  
71      public String getType() {
72          return type;
73      }
74  
75      public int getVersion() {
76          return version;
77      }
78  
79      /**
80       * @return Either a {@link UDAServiceType} or a more generic {@link ServiceType}.
81       */
82      public static ServiceType valueOf(String s) throws InvalidValueException {
83  
84          if (s == null)
85              throw new InvalidValueException("Can't parse null string");
86  
87          ServiceType serviceType = null;
88  
89          // Sometimes crazy UPnP devices deliver spaces in a URN, don't ask...
90          s = s.replaceAll("\\s", "");
91  
92          // First try UDAServiceType parse
93          try {
94              serviceType = UDAServiceType.valueOf(s);
95          } catch (Exception ex) {
96              // Ignore
97          }
98  
99          if (serviceType != null)
100             return serviceType;
101 
102         // Now try a generic ServiceType parse
103         try {
104             Matcher matcher = ServiceType.PATTERN.matcher(s);
105             if (matcher.matches() && matcher.groupCount() >= 3) {
106                 return new ServiceType(matcher.group(1), matcher.group(2), Integer.valueOf(matcher.group(3)));
107             }
108 
109             matcher = ServiceType.BROKEN_PATTERN.matcher(s);
110             if (matcher.matches() && matcher.groupCount() >= 3) {
111                 return new ServiceType(matcher.group(1), matcher.group(2), Integer.valueOf(matcher.group(3)));
112             }
113 
114             // TODO: UPNP VIOLATION: EyeTV Netstream uses colons in service type token
115             // urn:schemas-microsoft-com:service:pbda:tuner:1
116             matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):service:(.+?):([0-9]+).*").matcher(s);
117             if (matcher.matches() && matcher.groupCount() >= 3) {
118                 String cleanToken = matcher.group(2).replaceAll("[^a-zA-Z_0-9\\-]", "-");
119                 log.warning(
120                     "UPnP specification violation, replacing invalid service type token '"
121                         + matcher.group(2)
122                         + "' with: "
123                         + cleanToken
124                 );
125                 return new ServiceType(matcher.group(1), cleanToken, Integer.valueOf(matcher.group(3)));
126             }
127 
128             // TODO: UPNP VIOLATION: Ceyton InfiniTV uses colons in service type token and 'serviceId' instead of 'service'
129             // urn:schemas-opencable-com:serviceId:dri2:debug:1
130             matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):serviceId:(.+?):([0-9]+).*").matcher(s);
131             if (matcher.matches() && matcher.groupCount() >= 3) {
132                 String cleanToken = matcher.group(2).replaceAll("[^a-zA-Z_0-9\\-]", "-");
133                 log.warning(
134                     "UPnP specification violation, replacing invalid service type token '"
135                     + matcher.group(2)
136                     + "' with: "
137                     + cleanToken
138                 );
139                 return new ServiceType(matcher.group(1), cleanToken, Integer.valueOf(matcher.group(3)));
140             }
141         } catch (RuntimeException e) {
142             throw new InvalidValueException(String.format(
143                 "Can't parse service type string (namespace/type/version) '%s': %s", s, e.toString()
144             ));
145         }
146 
147         throw new InvalidValueException("Can't parse service type string (namespace/type/version): " + s);
148     }
149 
150     /**
151      * @return <code>true</code> if this type's namespace/name matches the other type's namespace/name and
152      *         this type's version is equal or higher than the given types version.
153      */
154     public boolean implementsVersion(ServiceType that) {
155         if (that == null) return false;
156         if (!namespace.equals(that.namespace)) return false;
157         if (!type.equals(that.type)) return false;
158         if (version < that.version) return false;
159         return true;
160     }
161 
162     public String toFriendlyString() {
163         return getNamespace() + ":" + getType() + ":" + getVersion();
164     }
165 
166     @Override
167     public String toString() {
168         return "urn:" + getNamespace() + ":service:" + getType() + ":" + getVersion();
169     }
170 
171     @Override
172     public boolean equals(Object o) {
173         if (this == o) return true;
174         if (o == null || !(o instanceof ServiceType)) return false;
175 
176         ServiceType that = (ServiceType) o;
177 
178         if (version != that.version) return false;
179         if (!namespace.equals(that.namespace)) return false;
180         if (!type.equals(that.type)) return false;
181 
182         return true;
183     }
184 
185     @Override
186     public int hashCode() {
187         int result = namespace.hashCode();
188         result = 31 * result + type.hashCode();
189         result = 31 * result + version;
190         return result;
191     }
192 }