001    /*
002     * Copyright (c) 2007-2014 Concurrent, Inc. All Rights Reserved.
003     *
004     * Project and contact information: http://www.cascading.org/
005     *
006     * This file is part of the Cascading project.
007     *
008     * Licensed under the Apache License, Version 2.0 (the "License");
009     * you may not use this file except in compliance with the License.
010     * You may obtain a copy of the License at
011     *
012     *     http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing, software
015     * distributed under the License is distributed on an "AS IS" BASIS,
016     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017     * See the License for the specific language governing permissions and
018     * limitations under the License.
019     */
020    
021    package cascading.util;
022    
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.io.UnsupportedEncodingException;
026    import java.net.InetAddress;
027    import java.net.MalformedURLException;
028    import java.net.URL;
029    import java.net.URLConnection;
030    import java.net.URLEncoder;
031    import java.util.Collections;
032    import java.util.Properties;
033    import java.util.Set;
034    import java.util.Timer;
035    import java.util.TimerTask;
036    import java.util.TreeSet;
037    
038    import cascading.flow.planner.PlatformInfo;
039    import cascading.property.AppProps;
040    import org.slf4j.Logger;
041    import org.slf4j.LoggerFactory;
042    
043    /**
044     *
045     */
046    public class Update extends TimerTask
047      {
048      private static final Logger LOG = LoggerFactory.getLogger( Update.class );
049      private static final String UPDATE_PROPERTIES = "latest.properties";
050    
051      public static final String UPDATE_CHECK_SKIP = "cascading.update.skip";
052      public static final String UPDATE_URL = "cascading.update.url";
053    
054      private static final Set<PlatformInfo> platformInfoSet = Collections.synchronizedSet( new TreeSet<PlatformInfo>() );
055      private static Timer timer;
056    
057      public static synchronized void checkForUpdate( PlatformInfo platformInfo )
058        {
059        if( Boolean.getBoolean( UPDATE_CHECK_SKIP ) )
060          return;
061    
062        if( platformInfo != null )
063          platformInfoSet.add( platformInfo );
064    
065        if( timer != null )
066          return;
067    
068        timer = new Timer( "UpdateRequestTimer", true );
069        timer.scheduleAtFixedRate( new Update(), 1000 * 30, 24 * 60 * 60 * 1000L );
070        }
071    
072      @Override
073      public void run()
074        {
075        checkForUpdate();
076        }
077    
078      public boolean checkForUpdate()
079        {
080        if( !Version.hasMajorMinorVersionInfo() )
081          return true;
082    
083        boolean isCurrentWip = Version.getReleaseFull() != null && Version.getReleaseFull().contains( "wip" );
084        boolean isCurrentDev = Version.getReleaseFull() == null || Version.getReleaseFull().contains( "wip-dev" );
085    
086        URL updateCheckUrl = getUpdateCheckUrl();
087    
088        if( updateCheckUrl == null )
089          return false;
090    
091        // do this before fetching latest.properties
092        if( isCurrentDev )
093          {
094          LOG.debug( "current release is dev build, update url: {}", updateCheckUrl.toString() );
095          return true;
096          }
097    
098        Properties latestProperties = getUpdateProperties( updateCheckUrl );
099    
100        if( latestProperties.isEmpty() )
101          return false;
102    
103        String latestMajor = latestProperties.getProperty( Version.CASCADING_RELEASE_MAJOR );
104        String latestMinor = latestProperties.getProperty( Version.CASCADING_RELEASE_MINOR );
105    
106        boolean isSameMajorRelease = equals( Version.getReleaseMajor(), latestMajor );
107        boolean isSameMinorRelease = equals( Version.getReleaseMinor(), latestMinor );
108    
109        if( isSameMajorRelease && isSameMinorRelease )
110          {
111          LOG.debug( "no updates available" );
112          return true;
113          }
114    
115        String version = latestProperties.getProperty( "cascading.release.version" );
116    
117        if( version == null )
118          LOG.debug( "release version info not found" );
119        else
120          LOG.info( "newer Cascading release available: {}", version );
121    
122        return true;
123        }
124    
125      private static Properties getUpdateProperties( URL updateUrl )
126        {
127        try
128          {
129          URLConnection connection = updateUrl.openConnection();
130          connection.setConnectTimeout( 3 * 1000 );
131    
132          InputStream in = connection.getInputStream();
133    
134          try
135            {
136            Properties props = new Properties();
137    
138            props.load( connection.getInputStream() );
139    
140            return props;
141            }
142          finally
143            {
144            if( in != null )
145              close( in );
146            }
147          }
148        catch( IOException exception )
149          {
150          LOG.debug( "unable to fetch latest properties", exception );
151          return new Properties();
152          }
153        }
154    
155      private static URL getUpdateCheckUrl()
156        {
157        String url = buildURL();
158    
159        String connector = url.indexOf( '?' ) > 0 ? "&" : "?";
160    
161        String spec = url + connector + buildParamsString();
162    
163        try
164          {
165          return new URL( spec );
166          }
167        catch( MalformedURLException exception )
168          {
169          LOG.debug( "malformed url: {}", spec, exception );
170          return null;
171          }
172        }
173    
174      private static String buildURL()
175        {
176        String baseURL = System.getProperty( UPDATE_URL, "" );
177    
178        if( baseURL.isEmpty() )
179          {
180          String releaseBuild = Version.getReleaseBuild();
181    
182          // if wip, only test if a newer wip version is available
183          if( releaseBuild != null && releaseBuild.contains( "wip" ) )
184            baseURL = "http://files.concurrentinc.com/cascading/";
185          else
186            baseURL = "http://files.cascading.org/cascading/";
187          }
188    
189        if( !baseURL.endsWith( "/" ) )
190          baseURL += "/";
191    
192        baseURL = String.format( "%s%s/%s", baseURL, Version.getReleaseMajor(), UPDATE_PROPERTIES );
193    
194        return baseURL;
195        }
196    
197      private static String buildParamsString()
198        {
199        StringBuilder sb = new StringBuilder();
200    
201        sb.append( "id=" );
202        sb.append( getClientId() );
203        sb.append( "&instance=" );
204        sb.append( urlEncode( AppProps.getApplicationID( null ) ) );
205        sb.append( "&os-name=" );
206        sb.append( urlEncode( getProperty( "os.name" ) ) );
207        sb.append( "&jvm-name=" );
208        sb.append( urlEncode( getProperty( "java.vm.name" ) ) );
209        sb.append( "&jvm-version=" );
210        sb.append( urlEncode( getProperty( "java.version" ) ) );
211        sb.append( "&os-arch=" );
212        sb.append( urlEncode( getProperty( "os.arch" ) ) );
213        sb.append( "&product=" );
214        sb.append( urlEncode( Version.CASCADING ) );
215        sb.append( "&version=" );
216        sb.append( urlEncode( Version.getReleaseFull() ) );
217        sb.append( "&version-build=" );
218        sb.append( urlEncode( Version.getReleaseBuild() ) );
219        sb.append( "&frameworks=" );
220        sb.append( urlEncode( getProperty( AppProps.APP_FRAMEWORKS ) ) );
221    
222        synchronized( platformInfoSet )
223          {
224          for( PlatformInfo platformInfo : platformInfoSet )
225            {
226            sb.append( "&platform-name=" );
227            sb.append( urlEncode( platformInfo.name ) );
228            sb.append( "&platform-version=" );
229            sb.append( urlEncode( platformInfo.version ) );
230            sb.append( "&platform-vendor=" );
231            sb.append( urlEncode( platformInfo.vendor ) );
232            }
233    
234          platformInfoSet.clear();
235          }
236    
237        return sb.toString();
238        }
239    
240      private static boolean equals( String lhs, String rhs )
241        {
242        return lhs != null && lhs.equals( rhs );
243        }
244    
245      private static int getClientId()
246        {
247        try
248          {
249          return Math.abs( InetAddress.getLocalHost().hashCode() );
250          }
251        catch( Throwable t )
252          {
253          return 0;
254          }
255        }
256    
257      private static String urlEncode( String param )
258        {
259        if( param == null )
260          return "";
261    
262        try
263          {
264          return URLEncoder.encode( param, "UTF-8" );
265          }
266        catch( UnsupportedEncodingException exception )
267          {
268          LOG.debug( "unable to encode param: {}", param, exception );
269    
270          return null;
271          }
272        }
273    
274      private static String getProperty( String prop )
275        {
276        return System.getProperty( prop, "" );
277        }
278    
279      private static void close( InputStream in )
280        {
281        try
282          {
283          in.close();
284          }
285        catch( IOException exception )
286          {
287          // do nothing
288          }
289        }
290      }