/* * Copyright (c) 2013 GigaSpaces Technologies Ltd. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package models; import java.io.File; import java.util.*; import javax.persistence.*; import javax.validation.ConstraintViolation; import javax.validation.Validator; import beans.Recipe; import beans.config.Conf; import beans.config.ServerConfig; import cloudify.widget.api.clouds.CloudProvider; import com.avaje.ebean.Junction; import models.query.QueryConf; import org.apache.commons.collections.Predicate; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.annotate.JsonBackReference; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonRootName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import play.data.validation.Constraints; import play.data.validation.Validation; import play.db.ebean.Model; import play.i18n.Messages; import play.libs.Json; import server.ApplicationContext; import server.exceptions.ServerException; import utils.CollectionUtils; import com.thoughtworks.xstream.annotations.XStreamAlias; import controllers.WidgetAdmin; import utils.StringUtils; /** * This class represents a widget metadata and relates to a specific {@link User}. * See {@link WidgetAdmin} * * @author Igor Goldenberg * @see WidgetAdmin */ @Entity @XStreamAlias("widget") @SuppressWarnings("serial") @JsonIgnoreProperties(ignoreUnknown = true) public class Widget extends Model { @Id private Long id; @Constraints.Required private String productName; @Constraints.Required private String providerURL; @Constraints.Required private String productVersion; @Constraints.Required private String title; private String youtubeVideoUrl; private boolean autoRefreshRecipe; private boolean autoRefreshProvider; public String cloudName; public String cloudProviderUrl; public String cloudProviderRootDir; @Enumerated(EnumType.STRING) public CloudProvider cloudProvider = CloudProvider.SOFTLAYER; public boolean sendEmail; // optional private String recipeURL = null; private Boolean allowAnonymous; @Constraints.Required private String apiKey; @JsonIgnore private Integer launches = 0; private Boolean enabled; public String loginsString; public String managerPrefix; @ManyToOne @JsonIgnore public MailChimpDetails mailChimpDetails; private Boolean showAdvanced; @JsonProperty(value="consolename") private String consoleName; @OneToOne( cascade = CascadeType.REMOVE ) public MandrillDetails mandrillDetails = null; // guy - this is a temporary work around until cloudify will sort // https://cloudifysource.atlassian.net/browse/CLOUDIFY-1258 private String recipeName; @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) private WidgetIcon icon; @JsonProperty(value="consoleurl") private String consoleURL; @JsonProperty( value="rootpath") private String recipeRootPath; private Boolean requireLogin; // should this widget support login? private String loginVerificationUrl = null; // url to verify user IDs. private String webServiceKey=null; // secret key we add on the web service calls. // private String theme; private long lifeExpectancy = 0; @JsonIgnore @ManyToOne( optional = false ) private User user; @Lob private String description; @Lob // use this field when you have information we don't need in the backend, only the front-end. private String data; // a schema less field to maintain more data about the widget @Version private long version = 0; // for remote bootstrap, use this service name to construct the console link. // private String consoleUrlService; @JsonIgnore @OneToMany(cascade=CascadeType.ALL, mappedBy = "widget") private List<WidgetInstance> instances = new LinkedList<WidgetInstance>( ); public static Finder<Long,Widget> find = new Finder<Long,Widget>(Long.class, Widget.class); private static Logger logger = LoggerFactory.getLogger( Widget.class ); @XStreamAlias("status") @JsonRootName("status") final static public class Status { /** * This class serves as status of the widget instance */ public final static String STATE_RUNNING = "running"; public final static String STATE_STOPPED = "stopped"; private State state = State.RUNNING; private List<String> output; private List<String> rawOutput; // for debug purposes private Integer timeleft; // minutes private Long timeleftMillis; // millis private String publicIp; private String instanceId; // server node instance id - NOT widget instance id. private Boolean remote; private Boolean hasPemFile; private WidgetInstance.ConsoleLink consoleLink; private String message; // for errors private Boolean instanceIsAvailable; // if install finished private Boolean cloudifyUiIsAvailable; private Boolean completed; // whether installation completed successfully public static enum State { STOPPED, RUNNING; } public Status() { } public void setCloudifyUiIsAvailable(Boolean cloudifyUiIsAvailable) { this.cloudifyUiIsAvailable = cloudifyUiIsAvailable; } public Boolean getCompleted() { return completed; } public void setCompleted(Boolean completed) { this.completed = completed; } public Boolean getInstanceIsAvailable() { return instanceIsAvailable; } public Boolean getCloudifyUiIsAvailable() { return cloudifyUiIsAvailable; } public void setInstanceIsAvailable(Boolean instanceIsAvailable) { this.instanceIsAvailable = instanceIsAvailable; } public void setConsoleLink(WidgetInstance.ConsoleLink link) { this.consoleLink = link; } public void setState(State state) { this.state = state; } public Long getTimeleftMillis() { return timeleftMillis; } public void setTimeleftMillis(Long timeleftMillis) { this.timeleftMillis = timeleftMillis; } public void setTimeleft(Integer timeleft) { this.timeleft = timeleft == null ? 0 : timeleft; } public Status setHasPemFile(Boolean hasPemFile) { this.hasPemFile = hasPemFile; return this; } public Status setRemote(Boolean remote) { this.remote = remote; return this; } public void setPublicIp(String publicIp) { this.publicIp = publicIp; } public void setMessage(String message) { this.message = message; } public String getMessage() { return message; } public Boolean getRemote() { return remote; } public Boolean getHasPemFile() { return hasPemFile; } public Status setInstanceId(String instanceId) { this.instanceId = instanceId; return this; } public void setOutput(List<String> output) { this.output = output; } public List<String> getRawOutput() { return rawOutput; } public void setRawOutput( List<String> rawOutput ) { this.rawOutput = rawOutput; } public List<String> getOutput() { if (CollectionUtils.isEmpty(output)) { output = new LinkedList<String>(); output.add(Messages.get("wait.while.preparing.env")); } return output; } public State getState() { return state; } public Integer getTimeleft() { return timeleft; } public String getPublicIp() { return publicIp; } public String getInstanceId() { return instanceId; } public WidgetInstance.ConsoleLink getConsoleLink() { return consoleLink; } @Override public String toString() { return "Status [state=" + state + ", output=" + ( ( output == null ) ? "NULL" : Arrays.toString( output.toArray( new String[ output.size() ] ) ) ) + ", rawOutput=" + ( ( rawOutput == null ) ? "NULL" : Arrays.toString( rawOutput.toArray( new String[ rawOutput.size() ] ) ) ) + ", timeleft=" + timeleft + ", timeleftMillis=" + timeleftMillis + ", publicIp=" + publicIp + ", instanceId=" + instanceId + ", remote=" + remote + ", hasPemFile=" + hasPemFile + ", consoleLink=" + consoleLink + ", message=" + message + ", instanceIsAvailable=" + instanceIsAvailable + ", cloudifyUiIsAvailable=" + cloudifyUiIsAvailable + ", completed=" + completed + "]"; } } // for serialization public Widget(){ } public Widget( String productName, String productVersion, String title, String youtubeVideoUrl, String providerURL, String recipeURL, String consoleName, String consoleURL, String recipeRootPath ) { this.productName = productName; this.title = title; this.productVersion = productVersion; this.youtubeVideoUrl = youtubeVideoUrl; this.providerURL = providerURL; this.recipeURL = recipeURL; this.consoleName = consoleName; this.consoleURL = consoleURL; this.recipeRootPath = recipeRootPath; init(); } public void init(){ this.enabled = true; this.launches = 0; this.apiKey = UUID.randomUUID().toString(); } public WidgetInstance addWidgetInstance( ServerNode serverNode, File recipeDir ) { WidgetInstance wInstance = new WidgetInstance(); if (recipeDir != null ){ Recipe.Type recipeType = new Recipe(recipeDir).getRecipeType(); wInstance.setRecipeType( recipeType ); } wInstance.setServerNode( serverNode ); wInstance.setInstallName( toInstallName() ); if (instances == null){ instances = new ArrayList<WidgetInstance>(); } refresh(); instances.add( wInstance ); save(); // server node has the foreign key.. serverNode.setWidgetInstance(wInstance); serverNode.save(); return wInstance; } public boolean hasRecipe(){ return !StringUtils.isEmptyOrSpaces(recipeURL); } public String toInstallName(){ if ( ApplicationContext.get().conf().features.autoGeneratedRecipeName.on ){ return ("" + productName + productVersion).toLowerCase().replaceAll( "[^a-z0-9]", "_" ); } else if ( recipeName == null ){ throw new RuntimeException( String.format("invalid state. all widgets should have a recipe name. recipe id [%s] does not have one", id) ); } return recipeName; } public boolean getShowAdvanced(){ return showAdvanced != null ? showAdvanced : true; } public String getShowAdvancedAsString(){ return getShowAdvanced() ? "true" : "false"; } @Deprecated // todo : DO NOT USE THIS.. this is strictly for "WidgetServerImpl". // todo : we should implement a different mechanism there, but for now there is no time. public static Widget getWidget( String apiKey ) { Widget widget = Widget.find.where().eq( "apiKey", apiKey ).findUnique(); if ( widget == null ) { String msg = Messages.get( "widget.apikey.not.valid", apiKey ); throw new ServerException( msg ).getResponseDetails().setError( msg ).done(); } return widget; } public String getConsoleName() { return consoleName; } public String getConsoleURL() { return consoleURL; } public static List<Widget> findByUser(User user) { return find.where().eq("user", user ).findList(); } public static Widget findByUserAndId( User user, Long widgetId ) { return find.where( ).eq( "user", user ).eq( "id",widgetId ).findUnique(); } public static Widget findById( Long widgetId ) { return find.where( ).eq( "id",widgetId ).findUnique(); } /** @return the widget by apiKey or null */ // guy - NOTE : we must always add "user" to the mix.. otherwise we never verify the user really owns the widget. static public Widget getWidgetByApiKey( User user, String apiKey ) { Widget widget = Widget.find.where().eq( "apiKey", apiKey ).eq( "user", user).findUnique(); if ( widget == null ) { String msg = Messages.get( "widget.apikey.not.valid", apiKey ); throw new ServerException( msg ).getResponseDetails().setError( msg ).done(); } return widget; } public Widget regenerateApiKey( ) { apiKey = UUID.randomUUID().toString(); save(); refresh(); return this; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } @JsonIgnore @Transient public List<WidgetInstance> getViableInstances(){ if ( CollectionUtils.isEmpty( instances )){ return new LinkedList<WidgetInstance>(); } List<WidgetInstance> result = new LinkedList<WidgetInstance>( instances ); CollectionUtils.filter( result, new Predicate() { @Override public boolean evaluate(Object o) { return !((WidgetInstance) o).isCorrupted(); } }); return result; } public List<WidgetInstance> getInstances() { return instances; } @JsonIgnore public void setInstances(List<WidgetInstance> instances) { this.instances = instances; } public int getLaunches() { return launches; } public void setConsoleName( String consoleName ) { this.consoleName = consoleName; } public void setConsoleURL( String consoleURL ) { this.consoleURL = consoleURL; } public void setRecipeRootPath( String recipeRootPath ) { this.recipeRootPath = recipeRootPath; } @JsonIgnore public void setLaunches(int launches) { this.launches = launches; } public String getRecipeURL() { return recipeURL; } public void setRecipeURL(String recipeURL) { this.recipeURL = recipeURL; } public String getProductName() { return productName; } public void setProductName(String productName) { this.productName = productName; } public String getCloudName() { return cloudName; } public void setCloudName(String cloudName) { this.cloudName = cloudName; } public String getProviderURL() { return providerURL; } public void setProviderURL(String providerURL) { this.providerURL = providerURL; } public String getProductVersion() { return productVersion; } public void setProductVersion(String productVersion) { this.productVersion = productVersion; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public boolean isYouku(){ return StringUtils.contains( youtubeVideoUrl, "youku" ); } public boolean isYoutube(){ return StringUtils.contains( youtubeVideoUrl, "/embed/" ); } // http://v.youku.com/v_show/id_XNzM4NzQzMTIw.html?from=y1.3-idx-grid-1519-9909.86808-86807.3-1 public String getYoukuVideoKey(){ try{ if ( StringUtils.isEmpty(youtubeVideoUrl)){ return null; } if ( StringUtils.contains(youtubeVideoUrl, "youku")){ return youtubeVideoUrl.split("/id_")[1].split(".html")[0]; } }catch(Exception e){ logger.error("error while getting youku key"); return null; } return null; } @JsonIgnore public String getYoutubeVideoKey(){ try{ if ( StringUtils.isEmpty( youtubeVideoUrl )){ return null; }else if ( StringUtils.contains( youtubeVideoUrl, "/embed/" ) ){ return youtubeVideoUrl.split( "/embed/" )[1]; }else{ return null; } }catch(Exception e){ logger.error( "error while getting youtube key from [{}]", youtubeVideoUrl ); return null; } } public String getYoutubeVideoUrl() { return youtubeVideoUrl; } public void setYoutubeVideoUrl(String youtubeVideoUrl) { this.youtubeVideoUrl = youtubeVideoUrl; } public Boolean getAllowAnonymous() { return allowAnonymous; } public void setAllowAnonymous(Boolean allowAnonymous) { this.allowAnonymous = allowAnonymous; } @JsonIgnore public void setLaunches(Integer launches) { this.launches = launches; } public Boolean isEnabled() { return enabled; } public Widget setEnabled( Boolean enabled ) { this.enabled = enabled; return this; } public String getRecipeRootPath() { return recipeRootPath; } public void countLaunch() { try { if (launches == null) { launches = 0; } launches++; save(); } catch (Exception e) { logger.warn("unable to count launch", e.getMessage()); } } @Override public String toString() { return String.format("Widget{id=%d, title='%s', apiKey='%s', launches=%d, enabled=%s, recipeRootPath='%s'}", id, title, apiKey, launches, enabled, recipeRootPath); } public long getLifeExpectancy() { // by default use configuration return lifeExpectancy == 0 ? ApplicationContext.get().conf().server.pool.expirationTimeMillis : lifeExpectancy ; } public void setLifeExpectancy(long lifeExpectancy) { ServerConfig.PoolConfiguration poolConf = ApplicationContext.get().conf().server.pool; lifeExpectancy = Math.max( lifeExpectancy, poolConf.minExpiryTimeMillis ); lifeExpectancy = Math.min(lifeExpectancy, poolConf.maxExpirationTimeMillis); this.lifeExpectancy = lifeExpectancy; } public String toDebugString() { return "Widget{" + "id=" + id + ", productName='" + productName + '\'' + ", title='" + title + '\'' + ", enabled=" + enabled + ", apiKey='" + apiKey + '\'' + ", user=" + user.toDebugString() + '}'; } @JsonBackReference // @JsonProperty public User getUser() { return user; } // guy - for display properties only! @JsonProperty public String getUsername(){ return user == null ? "null" : user.getEmail(); } @JsonIgnore public void setUsername( String username ){ } @JsonIgnore public void setNumOfInstances( int i){ } @Transient @JsonProperty public int getNumOfInstances(){ return instances.size(); } public boolean isRequiresLogin() { return requireLogin == Boolean.TRUE; // solves NPE } public boolean isAutoRefreshRecipe() { return autoRefreshRecipe; } public void setAutoRefreshRecipe(boolean autoRefreshRecipe) { this.autoRefreshRecipe = autoRefreshRecipe; } public boolean isAutoRefreshProvider() { return autoRefreshProvider; } public void setAutoRefreshProvider(boolean autoRefreshProvider) { this.autoRefreshProvider = autoRefreshProvider; } public String getCloudProviderRootDir() { return cloudProviderRootDir; } public void setCloudProviderRootDir(String cloudProviderRootDir) { this.cloudProviderRootDir = cloudProviderRootDir; } public String getCloudProviderUrl() { return cloudProviderUrl; } public void setCloudProviderUrl(String cloudProviderUrl) { this.cloudProviderUrl = cloudProviderUrl; } public Boolean getRequireLogin() { return requireLogin; } public void setRequireLogin(Boolean requireLogin) { this.requireLogin = requireLogin; } public String getLoginVerificationUrl() { return loginVerificationUrl; } public void setLoginVerificationUrl(String loginVerificationUrl) { this.loginVerificationUrl = loginVerificationUrl; } public String getWebServiceKey() { return webServiceKey; } public void setWebServiceKey(String webServiceKey) { this.webServiceKey = webServiceKey; } public void setDescription( String description ) { this.description = description; } public String getDescription() { return description; } public String getData() { return data; } public void setData(String data) { this.data = data; } @JsonIgnore public void setHasIcon( boolean hasIcon ){ } public boolean isHasIcon(){ return icon != null; // todo: verify this does not load "WidgetIcon" lazily. } public void setIcon( WidgetIcon icon ){ this.icon = icon; } public String getConsoleUrlService() { return consoleUrlService; } public void setConsoleUrlService( String consoleUrlService ) { this.consoleUrlService = consoleUrlService; } public String getRecipeName() { return recipeName; } public void setRecipeName( String recipeName ) { this.recipeName = recipeName; } public abstract static class IncludeInstancesMixin{ @JsonProperty("instances") public abstract List<WidgetInstance> getViableInstances(); } public static class WidgetQueryConfig extends QueryConf<WidgetQueryConfig.WidgetCriteria, Widget> { @Override protected WidgetCriteria newCriteria() { return new WidgetCriteria(); } @Override public Finder<Long, Widget> getFinder() { return Widget.find; } @Override protected void applyCriteria(WidgetCriteria criteria, Junction<Widget> conjunction) { if (criteria.enabled != null) { conjunction.eq("enabled", criteria.enabled); } if (criteria.user != null) { conjunction.eq("user", criteria.user); } } public class WidgetCriteria extends QueryConf<WidgetCriteria, Widget>.Criteria { private Boolean enabled; private User user; public WidgetCriteria setEnabled(Boolean enabled) { this.enabled = enabled; return this; } public WidgetCriteria setUser(User user) { this.user = user; return this; } } } public void setShowAdvanced(Boolean showAdvanced) { this.showAdvanced = showAdvanced; } public String getLoginsString() { return loginsString; } public void setLoginsString( String logins ){ loginsString = logins; } // public String getTheme() { // return theme; // } // // public void setTheme(String theme) { // this.theme = theme; // } @JsonIgnore public boolean hasCloudProviderData(){ try { if ( !StringUtils.isEmptyOrSpaces(data) ){ JsonNode parse = Json.parse(data); return parse.has("cloudProvider"); } }catch(Exception e){ logger.error("unable to check if I have cloud provider data", e); } return false; } @JsonIgnore public JsonNode getCloudProvideJson(){ if ( hasCloudProviderData() ){ return Json.parse(data).get("cloudProvider"); } return null; } }