001    /*
002     * Copyright (c) 2007-2015 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    import java.util.concurrent.atomic.AtomicInteger;
038    
039    import cascading.flow.planner.FlowPlanner;
040    import cascading.flow.planner.PlatformInfo;
041    import cascading.property.AppProps;
042    import org.slf4j.Logger;
043    import org.slf4j.LoggerFactory;
044    
045    /**
046     *
047     */
048    public class Update extends TimerTask
049      {
050      private static final Logger LOG = LoggerFactory.getLogger( Update.class );
051      private static final String UPDATE_PROPERTIES = "latest.properties";
052    
053      public static final String UPDATE_CHECK_SKIP = "cascading.update.skip";
054      public static final String UPDATE_URL = "cascading.update.url";
055    
056      private static final Set<String> plannerSet = Collections.synchronizedSet( new TreeSet<String>() );
057      private static final Set<PlatformInfo> platformInfoSet = Collections.synchronizedSet( new TreeSet<PlatformInfo>() );
058      private static Timer timer;
059      private static AtomicInteger requests = new AtomicInteger( 0 ); // successful requests
060      private static AtomicInteger pending = new AtomicInteger( 0 ); // pending requests
061    
062      // set foundation for checking planner versions from third-parties
063      public static void registerPlanner( Class<? extends FlowPlanner> plannerClass )
064        {
065        if( Boolean.getBoolean( UPDATE_CHECK_SKIP ) || plannerClass == null )
066          return;
067    
068        if( !plannerSet.add( plannerClass.getSimpleName() ) ) // not new
069          return;
070    
071        if( timer == null || pending.get() > 0 ) // already have updates queued, do not schedule
072          return;
073    
074        timer.schedule( new Update(), 1000 * 30 );
075        }
076    
077      public static synchronized void checkForUpdate( PlatformInfo platformInfo )
078        {
079        if( Boolean.getBoolean( UPDATE_CHECK_SKIP ) )
080          return;
081    
082        if( platformInfo != null )
083          platformInfoSet.add( platformInfo );
084    
085        if( timer != null )
086          return;
087    
088        timer = new Timer( "UpdateRequestTimer", true );
089        timer.scheduleAtFixedRate( new Update(), 1000 * 30, 24 * 60 * 60 * 1000L );
090        }
091    
092      private boolean hasUpdated = false;
093    
094      public Update()
095        {
096        pending.incrementAndGet(); // update queued
097        }
098    
099      @Override
100      public void run()
101        {
102        checkForUpdate();
103    
104        if( hasUpdated ) // don't count subsequent updates from this instance
105          return;
106    
107        hasUpdated = true;
108        pending.decrementAndGet(); // update completed
109        }
110    
111      public boolean checkForUpdate()
112        {
113        if( !Version.hasMajorMinorVersionInfo() )
114          return true;
115    
116        boolean isCurrentWip = Version.getReleaseFull() != null && Version.getReleaseFull().contains( "wip" );
117        boolean isCurrentDev = Version.getReleaseFull() == null || Version.getReleaseFull().contains( "wip-dev" );
118    
119        URL updateCheckUrl = getUpdateCheckUrl();
120    
121        if( updateCheckUrl == null )
122          return false;
123    
124        // do this before fetching latest.properties
125        if( isCurrentDev )
126          {
127          LOG.debug( "current release is dev build, update url: {}", updateCheckUrl.toString() );
128          return true;
129          }
130    
131        Properties latestProperties = getUpdateProperties( updateCheckUrl );
132    
133        if( latestProperties.isEmpty() )
134          return false;
135    
136        String latestMajor = latestProperties.getProperty( Version.CASCADING_RELEASE_MAJOR );
137        String latestMinor = latestProperties.getProperty( Version.CASCADING_RELEASE_MINOR );
138    
139        boolean isSameMajorRelease = equals( Version.getReleaseMajor(), latestMajor );
140        boolean isSameMinorRelease = equals( Version.getReleaseMinor(), latestMinor );
141    
142        if( isSameMajorRelease && isSameMinorRelease )
143          {
144          LOG.debug( "no updates available" );
145          return true;
146          }
147    
148        String version = latestProperties.getProperty( "cascading.release.version" );
149    
150        if( version == null )
151          LOG.debug( "release version info not found" );
152        else
153          LOG.info( "newer Cascading release available: {}", version );
154    
155        return true;
156        }
157    
158      private static Properties getUpdateProperties( URL updateUrl )
159        {
160        try
161          {
162          URLConnection connection = updateUrl.openConnection();
163          connection.setConnectTimeout( 3 * 1000 );
164    
165          InputStream inputStream = connection.getInputStream();
166    
167          try
168            {
169            Properties props = new Properties();
170    
171            if( inputStream != null )
172              {
173              props.load( inputStream );
174              requests.incrementAndGet();
175              }
176    
177            return props;
178            }
179          finally
180            {
181            close( inputStream );
182            }
183          }
184        catch( IOException exception )
185          {
186          // do not reschedule requests
187          LOG.debug( "unable to fetch latest properties", exception );
188          return new Properties();
189          }
190        }
191    
192      private static URL getUpdateCheckUrl()
193        {
194        String url = buildURL();
195    
196        String connector = url.indexOf( '?' ) > 0 ? "&" : "?";
197    
198        String spec = url + connector + buildParamsString();
199    
200        try
201          {
202          return new URL( spec );
203          }
204        catch( MalformedURLException exception )
205          {
206          LOG.debug( "malformed url: {}", spec, exception );
207          return null;
208          }
209        }
210    
211      private static String buildURL()
212        {
213        String baseURL = System.getProperty( UPDATE_URL, "" );
214    
215        if( baseURL.isEmpty() )
216          {
217          String releaseBuild = Version.getReleaseBuild();
218    
219          // if wip, only test if a newer wip version is available
220          if( releaseBuild != null && releaseBuild.contains( "wip" ) )
221            baseURL = "http://files.concurrentinc.com/cascading/";
222          else
223            baseURL = "http://files.cascading.org/cascading/";
224          }
225    
226        if( !baseURL.endsWith( "/" ) )
227          baseURL += "/";
228    
229        baseURL = String.format( "%s%s/%s", baseURL, Version.getReleaseMajor(), UPDATE_PROPERTIES );
230    
231        return baseURL;
232        }
233    
234      private static String buildParamsString()
235        {
236        StringBuilder sb = new StringBuilder();
237    
238        sb.append( "id=" );
239        sb.append( getClientId() );
240        sb.append( "&instance=" );
241        sb.append( urlEncode( AppProps.getApplicationID( null ) ) );
242        sb.append( "&request=" );
243        sb.append( requests.get() );
244        sb.append( "&os-name=" );
245        sb.append( urlEncode( getProperty( "os.name" ) ) );
246        sb.append( "&jvm-name=" );
247        sb.append( urlEncode( getProperty( "java.vm.name" ) ) );
248        sb.append( "&jvm-version=" );
249        sb.append( urlEncode( getProperty( "java.version" ) ) );
250        sb.append( "&os-arch=" );
251        sb.append( urlEncode( getProperty( "os.arch" ) ) );
252        sb.append( "&product=" );
253        sb.append( urlEncode( Version.CASCADING ) );
254        sb.append( "&version=" );
255        sb.append( urlEncode( Version.getReleaseFull() ) );
256        sb.append( "&version-build=" );
257        sb.append( urlEncode( Version.getReleaseBuild() ) );
258        sb.append( "&frameworks=" );
259        sb.append( urlEncode( getProperty( AppProps.APP_FRAMEWORKS ) ) );
260    
261        synchronized( plannerSet )
262          {
263          for( String plannerName : plannerSet )
264            {
265            sb.append( "&planner-name=" );
266            sb.append( urlEncode( plannerName ) );
267            }
268          }
269    
270        synchronized( platformInfoSet )
271          {
272          for( PlatformInfo platformInfo : platformInfoSet )
273            {
274            sb.append( "&platform-name=" );
275            sb.append( urlEncode( platformInfo.name ) );
276            sb.append( "&platform-version=" );
277            sb.append( urlEncode( platformInfo.version ) );
278            sb.append( "&platform-vendor=" );
279            sb.append( urlEncode( platformInfo.vendor ) );
280            }
281          }
282    
283        return sb.toString();
284        }
285    
286      private static boolean equals( String lhs, String rhs )
287        {
288        return lhs != null && lhs.equals( rhs );
289        }
290    
291      private static int getClientId()
292        {
293        try
294          {
295          return Math.abs( InetAddress.getLocalHost().hashCode() );
296          }
297        catch( Throwable t )
298          {
299          return 0;
300          }
301        }
302    
303      private static String urlEncode( String param )
304        {
305        if( param == null )
306          return "";
307    
308        try
309          {
310          return URLEncoder.encode( param, "UTF-8" );
311          }
312        catch( UnsupportedEncodingException exception )
313          {
314          LOG.debug( "unable to encode param: {}", param, exception );
315    
316          return null;
317          }
318        }
319    
320      private static String getProperty( String prop )
321        {
322        return System.getProperty( prop, "" );
323        }
324    
325      private static void close( InputStream in )
326        {
327        try
328          {
329          if( in != null )
330            in.close();
331          }
332        catch( IOException exception )
333          {
334          // do nothing
335          }
336        }
337      }