001/**
002 * Copyright 2015 Tampere University of Technology, Pori Department
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package service.tut.pori.apilta.files;
017
018import java.io.Closeable;
019import java.io.IOException;
020import java.util.Date;
021import java.util.Map;
022import java.util.Properties;
023import java.util.Set;
024
025import org.apache.log4j.Logger;
026import org.jclouds.Constants;
027import org.jclouds.ContextBuilder;
028import org.jclouds.http.HttpResponseException;
029import org.jclouds.io.Payload;
030import org.jclouds.openstack.swift.v1.SwiftApi;
031import org.jclouds.openstack.swift.v1.TemporaryUrlSigner;
032import org.jclouds.openstack.swift.v1.domain.SwiftObject;
033import org.jclouds.openstack.swift.v1.features.AccountApi;
034import org.jclouds.openstack.swift.v1.features.ContainerApi;
035import org.jclouds.openstack.swift.v1.features.ObjectApi;
036
037import core.tut.pori.context.ServiceInitializer;
038import core.tut.pori.http.Definitions;
039import service.tut.pori.apilta.ApiltaProperties;
040import service.tut.pori.apilta.files.datatypes.SignedURL;
041
042/**
043 * Template class for accessing a pre-configured swift instance
044 * 
045 */
046public class SwiftTemplate implements Closeable {
047  private static final long CHECK_API_INTERVAL = 1800L;
048  private static final Logger LOGGER = Logger.getLogger(SwiftTemplate.class);
049  private static final String PARAMETER_TEMP_URI_SIGNATURE = "temp_url_sig";
050  private static final String PARAMETER_TEMP_URI_EXPIRATION = "temp_url_expires";
051  private boolean _checkContainer = true;
052  private ObjectApi _objectApi = null;
053  private ApiltaProperties _properties = null;
054  private Set<String> _regions = null;
055  private SwiftApi _swiftApi = null;
056  private TemporaryUrlSigner _tempUriSigner = null;
057  
058  /**
059   * 
060   */
061  public SwiftTemplate() {
062    initialize();
063  }
064  
065  /**
066   * 
067   */
068  private void initialize(){
069    LOGGER.debug("Initializing Swift handler...");
070    
071    _properties = ServiceInitializer.getPropertyHandler().getSystemProperties(ApiltaProperties.class);
072    
073    Properties overrides = new Properties();
074    overrides.setProperty(Constants.PROPERTY_RETRY_DELAY_START, 500 + "");
075    overrides.setProperty(Constants.PROPERTY_MAX_RETRIES, 5 + "");
076    
077    _swiftApi = ContextBuilder.newBuilder(_properties.getProvider())
078          .endpoint(_properties.getEndpoint())
079          .credentials(_properties.getIdentity(), _properties.getCredential())
080          .overrides(overrides)
081          .buildApi(SwiftApi.class);
082  }
083
084  @Override
085  public void close() {
086    LOGGER.debug("Closing...");
087    
088    _tempUriSigner = null;
089    
090    _objectApi = null;
091    try {
092      _swiftApi.close();
093    } catch (IOException ex) {
094      LOGGER.error(ex, ex);
095    }
096    _swiftApi = null;
097
098    //reset members
099    _regions = null;
100    _checkContainer = true;
101  }
102  
103  /**
104   * 
105   * @param objectName
106   * @return details for the object matching the given GUID or null if object was not found
107   */
108  public SwiftObject get(String objectName) {
109    return getObjectApi().getWithoutBody(objectName); //TODO add try-catch for connection refused situation, maybe try to change to another region?
110  }
111
112  /**
113   * 
114   * @param objectName
115   */
116  public void delete(String objectName) {
117    getObjectApi().delete(objectName);  //TODO add try-catch for connection refused situation, maybe try to change to another region?
118  }
119
120  /**
121   * Returns the object for accessing files on Swift storage based on resolved region and configure container.
122   * @return Swift Object API
123   */
124  private ObjectApi getObjectApi(){
125    //TODO: the _objectApi (like any other swift API) might get stuck for some unknown reason. The best effort work around for this could just be re-instantiating the whole SwiftTemplate.
126    if(_objectApi == null){ //empty on first call as it is loaded on the first use of swift template
127      if(_checkContainer){ //just a safe guard to make sure required container exists
128        prepareContainer();
129      }
130      _objectApi = _swiftApi.getObjectApi(resolveRegion(), _properties.getContainer());
131    }
132    return _objectApi;
133  }
134  
135  /**
136   * <p>Checks for the required container, and creates it if it doesn't exist.</p>
137   * <p>Make the method synchronized just in case.</p>
138   */
139  private synchronized void prepareContainer(){
140    _checkContainer = false;  //disable future checks for container
141    //Make sure that the needed container exists
142    ContainerApi containerApi = _swiftApi.getContainerApi(resolveRegion());
143    containerApi.create(_properties.getContainer());  //returns true if the container was created, false if the container already existed.
144    //regardless of the return value of the previous method call, the requested container should now be available
145  }
146  
147  /**
148   * <p>This function returns the preferred region for this service instance if configured. 
149   * Otherwise the first configured region is returned.</p>
150   * <p>Note that this function does not support multi region usage, i.e. the same selected region is used until the end of the world.</p>
151   * @return region name
152   */
153  private String resolveRegion(){
154    if(_regions == null){
155      try{
156        _regions = _swiftApi.getConfiguredRegions();
157      }catch(HttpResponseException ex){
158        LOGGER.error(ex, ex);
159        throw new RuntimeException("SwiftAPI refused connection.");
160      }
161    }
162    if(_regions.contains(_properties.getPreferredRegion())){
163      return _properties.getPreferredRegion();  //return the preferred region, if available
164    }else{
165      return _regions.iterator().next();  //return the first region in any other case (as a default choice)
166    }
167  }
168
169  /**
170   * 
171   * @param objectName
172   * @param payload
173   * @return ETag of the object
174   */
175  public String put(String objectName, Payload payload) {
176    return getObjectApi().put(objectName, payload); //TODO add try-catch for connection refused situation, maybe try to change to another region?
177  }
178
179  /**
180   * 
181   * @param object 
182   * @return temporary url signed by this template
183   */
184  public SignedURL signURL(SwiftObject object) {
185    if(_tempUriSigner == null){
186      AccountApi accountApi = _swiftApi.getAccountApi(resolveRegion());
187      _tempUriSigner = TemporaryUrlSigner.checkApiEvery(accountApi, CHECK_API_INTERVAL);
188    }
189    long expires = System.currentTimeMillis() / 1000 + _properties.getDefaultExpirationTime();
190    String signature = _tempUriSigner.sign(Definitions.METHOD_GET, object.getUri().getPath(), expires);
191    //TODO for some reason jclouds want to use public ip instead of internal ip when communicating service endpoints, which is kinda silly
192    //TODO Also this workaround defeats the point of having multiple regions (as it is hardcoded to a one endpoint).
193    return new SignedURL(_properties.getPublicEndpoint()+object.getUri().getPath()+Definitions.SEPARATOR_URI_METHOD_PARAMS+PARAMETER_TEMP_URI_SIGNATURE+Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE+signature+Definitions.SEPARATOR_URI_QUERY_PARAMS+PARAMETER_TEMP_URI_EXPIRATION+Definitions.SEPARATOR_URI_QUERY_PARAM_VALUE+expires, new Date(expires*1000));  
194  }
195  
196  /**
197   * 
198   * @param objectName 
199   * @param metadata 
200   */
201  public void update(String objectName, Map<String, String> metadata){
202    getObjectApi().updateMetadata(objectName, metadata); //TODO check what this does if the objectName is invalid or non-existent
203  }
204}